Skip to content

If a record has multiple constructors without an explicit constructor chosen, default to the primary constructor #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
31 changes: 27 additions & 4 deletions src/Dapper.AOT.Analyzers/Internal/Inspection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,8 @@ internal static ConstructorResult ChooseConstructor(ITypeSymbol? typeSymbol, out
return ConstructorResult.SuccessSingleExplicit;
}

List<IMethodSymbol> implicitCtors = new();

// look for remaining constructors
foreach (var ctor in ctors)
{
Expand Down Expand Up @@ -883,13 +885,34 @@ internal static ConstructorResult ChooseConstructor(ITypeSymbol? typeSymbol, out
continue;
}

if (constructor is not null)
implicitCtors.Add(ctor);
}

if (implicitCtors.Count == 0)
{
return ConstructorResult.NoneFound;
}

if (implicitCtors.Count == 1)
{
constructor = implicitCtors[0];
return ConstructorResult.SuccessSingleImplicit;
}

// if it's a record without an [ExplicitConstructor] use the primary constructor, as you can't assign the attribute to the primary constructor
if (named.IsRecord)
{
bool hasPrimaryConstructor = named.TryGetPrimaryConstructor(out var primaryConstructor);

if (hasPrimaryConstructor && primaryConstructor != null)
{
return ConstructorResult.FailMultipleImplicit;
constructor = primaryConstructor;
return ConstructorResult.SuccessSingleImplicit;
}
constructor = ctor;
}
return constructor is null ? ConstructorResult.NoneFound : ConstructorResult.SuccessSingleImplicit;

constructor = implicitCtors[0];
return ConstructorResult.FailMultipleImplicit;
}

/// <summary>
Expand Down
30 changes: 27 additions & 3 deletions src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Dapper.Internal.Roslyn;

Expand All @@ -13,7 +14,7 @@ internal static class TypeSymbolExtensions
Func<IMethodSymbol, bool>? filter = null)
{
if (typeSymbol is null) yield break;

foreach (var methodSymbol in typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.Where(m => m.MethodKind is MethodKind.Ordinary or MethodKind.DeclareMethod))
Expand Down Expand Up @@ -202,7 +203,7 @@ public static bool ImplementsIReadOnlyCollection(this ITypeSymbol? typeSymbol, o
/// True, if passed <param name="typeSymbol"/> implements <see cref="IReadOnlyList{T}"/>. False otherwise
/// </returns>
/// <param name="searchedTypeSymbol">if found, an interface type symbol</param>
public static bool ImplementsIReadOnlyList(this ITypeSymbol? typeSymbol, out ITypeSymbol? searchedTypeSymbol)
public static bool ImplementsIReadOnlyList(this ITypeSymbol? typeSymbol, out ITypeSymbol? searchedTypeSymbol)
=> typeSymbol.ImplementsInterface(SpecialType.System_Collections_Generic_IReadOnlyList_T, out searchedTypeSymbol);

private static bool ImplementsInterface(
Expand Down Expand Up @@ -254,4 +255,27 @@ private static bool ImplementsInterface(
found = null;
return false;
}
}

internal static bool TryGetPrimaryConstructor(
this INamedTypeSymbol typeSymbol,
out IMethodSymbol? primaryConstructor)
{
// from Roslyn
// https://github.com/dotnet/roslyn/blob/b6d2dc0d1be19ea0169f710bb370479734f746b9/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ITypeSymbolExtensions.cs#L40
if (typeSymbol.TypeKind is TypeKind.Class or TypeKind.Struct)
{
// A bit hacky to determine the parameters of primary constructor associated with a given record.
// Simplifying is tracked by: https://github.com/dotnet/roslyn/issues/53092. Note: When the issue is
// handled, we can remove the logic here and handle things in GetParameters extension. BUT if GetParameters
// extension method gets updated to handle records, we need to test EVERY usage of the extension method and
// make sure the change is applicable to all these usages.

primaryConstructor = typeSymbol.InstanceConstructors.FirstOrDefault(
c => c.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is TypeDeclarationSyntax);
return primaryConstructor is not null;
}

primaryConstructor = null;
return false;
}
}
10 changes: 10 additions & 0 deletions test/Dapper.AOT.Test/Verifiers/DAP036.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public void Foo(DbConnection conn)
_ = conn.Query<MultipleImplicit>("storedproc");

_ = conn.Query<RecordClass>("storedproc");
_ = conn.Query<RecordClassWithMultipleConstructors>("storedproc");
_ = conn.Query<RecordStruct>("storedproc");
_ = conn.Query<RecordStructWithMultipleConstructors>("storedproc");
_ = conn.Query<ReadOnlyRecordStruct>("storedproc");
}

Expand Down Expand Up @@ -51,7 +53,15 @@ public MultipleImplicit(string b) {}
public MultipleImplicit(MultipleImplicit c) {}
}
record class RecordClass(int a);
public record class RecordClassWithMultipleConstructors(int a, bool b)
{
public RecordClassWithMultipleConstructors(int a) : this(a, false) {}
}
record struct RecordStruct(int a);
public record struct RecordStructWithMultipleConstructors(int a, bool b)
{
public RecordStructWithMultipleConstructors(int a) : this(a, false) {}
}
readonly record struct ReadOnlyRecordStruct(int a);

namespace System.Runtime.CompilerServices
Expand Down