S1144/S4487 results in AD0001

The following piece of code results in an AD0001:

/// <summary>
/// Provides a base implementation for JSON converters of structs that may or may not be nullable.
/// </summary>
/// <typeparam name="T">The type of structs to convert.</typeparam>
[Inheritable]
public class NullableStructJsonConverter<T> : JsonConverterFactory
    where T : struct
{
    private static readonly JsonSerializerOptions FallbackOptions = new(JsonSerializerDefaults.Web);
    private readonly Nullable nullable;
    private readonly NotNullable notNullable;

    /// <summary>
    /// Initializes a new instance of the <see cref="NullableStructJsonConverter{T}"/> class.
    /// </summary>
    protected NullableStructJsonConverter()
    {
        nullable = new(this);
        notNullable = new(this);
    }

    /// <inheritdoc />
    [Pure]
    public override bool CanConvert(Type typeToConvert)
        => typeToConvert == typeof(T) || typeToConvert == typeof(T?);

    /// <inheritdoc />
    [Pure]
    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => this switch
    {
        _ when typeToConvert == typeof(T) => notNullable,
        _ when typeToConvert == typeof(T?) => nullable,
        _ => null,
    };

    /// <inheritdoc cref="JsonConverter{T}.Read(ref Utf8JsonReader, Type, JsonSerializerOptions)" />
    [Pure]
    public virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => ReadNullable(ref reader, typeToConvert, options) ?? default;

    /// <inheritdoc cref="Read(ref Utf8JsonReader, Type, JsonSerializerOptions)" />
    [Pure]
    public virtual T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => JsonSerializer.Deserialize<T?>(ref reader, FallbackOptions);

    /// <inheritdoc cref="JsonConverter{T}.Write(Utf8JsonWriter, T, JsonSerializerOptions)" />
    public virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        => JsonSerializer.Serialize(writer, value, FallbackOptions);

    /// <inheritdoc cref="Write(Utf8JsonWriter, T, JsonSerializerOptions)" />
    public virtual void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
        => JsonSerializer.Serialize(writer, value, FallbackOptions);

    private sealed class NotNullable(NullableStructJsonConverter<T> converter) : JsonConverter<T>
    {
        /// <inheritdoc />
        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            => converter.Read(ref reader, typeToConvert, options);

        /// <inheritdoc />
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
            => converter.Write(writer, value, FallbackOptions);
    }

    private sealed class Nullable(NullableStructJsonConverter<T> converter) : JsonConverter<T?>
    {
        /// <inheritdoc />
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            => converter.ReadNullable(ref reader, typeToConvert, options);

        /// <inheritdoc />
        public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
            => converter.Write(writer, value, FallbackOptions);
    }
}

With an detailed error:

Analyzer 'SonarAnalyzer.CSharp.Rules.UnusedPrivateMember' threw an exception of type 'System.IndexOutOfRangeException' with message 'Index was outside the bounds of the array.'.
Exception occurred with following context:
Compilation: *****
ISymbol: NullableStructJsonConverter (NamedType)

System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Collections.Immutable.ImmutableArray`1.get_Item(Int32 index)
   at SonarAnalyzer.CSharp.Core.Wrappers.ObjectCreationFactory.ImplicitObjectCreation.TypeAsString(SemanticModel semanticModel)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.Visit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.DefaultVisit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.VisitAssignmentExpression(AssignmentExpressionSyntax node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.Visit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.Visit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.DefaultVisit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.Visit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.Visit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.DefaultVisit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.Visit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.Visit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.DefaultVisit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.Visit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.Visit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.DefaultVisit(SyntaxNode node)
   at Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker.Visit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Syntax.Utilities.SymbolUsageCollector.Visit(SyntaxNode node)
   at SonarAnalyzer.CSharp.Core.Syntax.Utilities.SafeCSharpSyntaxWalker.SafeVisit(SyntaxNode syntaxNode)
   at SonarAnalyzer.CSharp.Rules.UnusedPrivateMember.<>c__DisplayClass28_0.<VisitDeclaringReferences>b__1(SyntaxReference x)
   at System.Linq.Enumerable.All[TSource](IEnumerable`1 source, Func`2 predicate)
   at SonarAnalyzer.CSharp.Rules.UnusedPrivateMember.VisitDeclaringReferences(ISymbol symbol, ISafeSyntaxWalker visitor, SonarSymbolReportingContext context, Boolean includeGeneratedFile)
   at SonarAnalyzer.CSharp.Rules.UnusedPrivateMember.NamedSymbolAction(SonarSymbolReportingContext context, HashSet`1 removableInternalTypes)
   at SonarAnalyzer.CSharp.Rules.UnusedPrivateMember.<>c__DisplayClass14_0.<Initialize>b__1(SonarSymbolReportingContext x)
   at SonarAnalyzer.Core.AnalysisContext.SonarCompilationStartAnalysisContext.<>c__DisplayClass11_0.<RegisterSymbolAction>b__0(SymbolAnalysisContext x)
   at Microsoft.CodeAnalysis.Diagnostics.AnalyzerExecutor.ExecuteAndCatchIfThrows_NoLock[TArg](DiagnosticAnalyzer analyzer, Action`1 analyze, TArg argument, Nullable`1 info, CancellationToken cancellationToken)
-----

Suppress the following diagnostics to disable this analyzer: S1144, S4487

Reproducable with Release 10.23 · SonarSource/sonar-dotnet · GitHub

Minor note: sorry, I had no time to try to reduce the snippet to a smaller reproducer, I assume that will be possible./

Hey @Corniel,

Thanks for the report!
The AD0001 is caused by the nested Nullable class.
It seems one of our method assumes that any class called Nullable will have a type argument (e.g. System.Nullable<T>).

I think the minimal repro for S1144 is:


public class Outer<T>
{
    private readonly Nullable field;
    public Outer() => field = new(this);

    private sealed class Nullable(Outer<T> outer)
    {
        public object Value => outer;
    }
}

I’ll get started on a fix.

@alexander.meseldzija Given your response, I assume you could reproduce it, fortunately. Good luck with the fix.