Skip to content
Draft
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 @@ -14,6 +14,9 @@ namespace EntityFrameworkCore.Projectables.CodeFixes;
/// Inserts a <c>public ClassName() { }</c> constructor into the class that carries the
/// <c>[Projectable]</c> constructor, satisfying the object-initializer requirement of the
/// generated expression tree.
/// When all containing type declarations are <c>partial</c> (inline generation mode) a second
/// action offering a <c>private ClassName() { }</c> constructor is also registered, since
/// the inline accessor is generated inside the class and can access private constructors.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingParameterlessConstructorCodeFixProvider))]
[Shared]
Expand Down Expand Up @@ -44,17 +47,56 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)

var typeName = typeDecl.Identifier.Text;

// Always offer the public constructor fix.
context.RegisterCodeFix(
CodeAction.Create(
title: $"Add parameterless constructor to '{typeName}'",
createChangedDocument: ct => AddParameterlessConstructorAsync(context.Document, typeDecl, ct),
createChangedDocument: ct => AddParameterlessConstructorAsync(
context.Document, typeDecl, SyntaxKind.PublicKeyword, ct),
equivalenceKey: "EFP0008_AddParameterlessConstructor"),
diagnostic);

// When the full containing-type hierarchy is partial the accessor is generated inline
// inside the class; private constructors are then accessible from the accessor.
// Offer an additional private constructor fix in that case.
var isFullHierarchyPartial = IsFullHierarchyPartial(typeDecl);
if (isFullHierarchyPartial)
{
context.RegisterCodeFix(
CodeAction.Create(
title: $"Add private parameterless constructor to '{typeName}'",
createChangedDocument: ct => AddParameterlessConstructorAsync(
context.Document, typeDecl, SyntaxKind.PrivateKeyword, ct),
equivalenceKey: "EFP0008_AddPrivateParameterlessConstructor"),
diagnostic);
}
}

/// <summary>
/// Returns <c>true</c> when <paramref name="typeDecl"/> and every ancestor
/// <see cref="TypeDeclarationSyntax"/> all carry the <c>partial</c> modifier,
/// meaning the Roslyn generator will use inline generation for this type.
/// </summary>
private static bool IsFullHierarchyPartial(TypeDeclarationSyntax typeDecl)
{
SyntaxNode? current = typeDecl;
while (current is TypeDeclarationSyntax tds)
{
if (!tds.Modifiers.Any(SyntaxKind.PartialKeyword))
{
return false;
}

current = tds.Parent;
}

return true;
}

private async static Task<Document> AddParameterlessConstructorAsync(
Document document,
TypeDeclarationSyntax typeDecl,
SyntaxKind accessibilityKeyword,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -66,7 +108,7 @@ private async static Task<Document> AddParameterlessConstructorAsync(
var parameterlessCtor = SyntaxFactory
.ConstructorDeclaration(typeDecl.Identifier.WithoutTrivia())
.WithModifiers(SyntaxFactory.TokenList(
SyntaxFactory.Token(SyntaxKind.PublicKeyword)
SyntaxFactory.Token(accessibilityKeyword)
.WithTrailingTrivia(SyntaxFactory.Space)))
.WithParameterList(SyntaxFactory.ParameterList())
.WithBody(SyntaxFactory.Block())
Expand All @@ -81,4 +123,3 @@ private async static Task<Document> AddParameterlessConstructorAsync(
return document.WithSyntaxRoot(newRoot);
}
}

Original file line number Diff line number Diff line change
@@ -1 +1,6 @@

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|---------------------------------------------------------------------------------------------------------------
EFP0013 | Design | Info | Containing class should be partial to allow [Projectable] inline accessor with private/protected member access

Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,12 @@ static internal class Diagnostics
category: "Design",
DiagnosticSeverity.Info,
isEnabledByDefault: true);
}

public readonly static DiagnosticDescriptor ContainingClassShouldBePartial = new DiagnosticDescriptor(
id: "EFP0013",
title: "Containing class should be partial",
messageFormat: "Class '{0}' should be declared as 'partial' to allow [Projectable] to generate the expression accessor inside the class, enabling access to private and protected members",
category: "Design",
DiagnosticSeverity.Info,
isEnabledByDefault: true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -388,12 +388,24 @@ private static bool TryApplyConstructorBody(

// Verify the containing type has an accessible parameterless (instance) constructor.
// The generated projection is: new T() { Prop = ... }, which requires one.
//
// When every containing type declaration is partial the accessor is generated inline
// inside the class itself, where private and protected constructors are accessible.
var containingTypeDecls = constructorDeclarationSyntax.Ancestors()
.OfType<TypeDeclarationSyntax>()
.ToList();
var isInlineContext = containingTypeDecls.Count > 0
&& containingTypeDecls.All(t => t.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));

var hasAccessibleParameterlessConstructor = containingType.Constructors
.Any(c => !c.IsStatic
&& c.Parameters.IsEmpty
&& c.DeclaredAccessibility is Accessibility.Public
or Accessibility.Internal
or Accessibility.ProtectedOrInternal);
&& (c.DeclaredAccessibility is Accessibility.Public
or Accessibility.Internal
or Accessibility.ProtectedOrInternal
|| (isInlineContext
&& c.DeclaredAccessibility is Accessibility.Private
or Accessibility.Protected)));

if (!hasAccessibleParameterlessConstructor)
{
Expand Down
Loading
Loading