Skip to content
Closed
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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="ScenarioTests.XUnit" Version="1.0.1" />
Expand Down
126 changes: 111 additions & 15 deletions src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,26 @@ x is IPropertySymbol xProperty &&
var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, nullConditionalRewriteSupport, semanticModel, context);
var declarationSyntaxRewriter = new DeclarationSyntaxRewriter(semanticModel);

// For C# 14 extension blocks, the containing type is a nested type with a name like "extension ( T )"
// We need to use the outer containing type instead
var effectiveContainingType = memberSymbol.ContainingType;
if (effectiveContainingType.Name.StartsWith("extension", StringComparison.Ordinal) &&
effectiveContainingType.ContainingType is not null)
{
effectiveContainingType = effectiveContainingType.ContainingType;
}

var descriptor = new ProjectableDescriptor {

UsingDirectives = member.SyntaxTree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>(),
ClassName = memberSymbol.ContainingType.Name,
ClassNamespace = memberSymbol.ContainingType.ContainingNamespace.IsGlobalNamespace ? null : memberSymbol.ContainingType.ContainingNamespace.ToDisplayString(),
ClassName = effectiveContainingType.Name,
ClassNamespace = effectiveContainingType.ContainingNamespace.IsGlobalNamespace ? null : effectiveContainingType.ContainingNamespace.ToDisplayString(),
MemberName = memberSymbol.Name,
NestedInClassNames = GetNestedInClassPath(memberSymbol.ContainingType),
NestedInClassNames = GetNestedInClassPath(effectiveContainingType),
ParametersList = SyntaxFactory.ParameterList()
};

if (memberSymbol.ContainingType is INamedTypeSymbol { IsGenericType: true } containingNamedType)
if (effectiveContainingType is INamedTypeSymbol { IsGenericType: true } containingNamedType)
{
descriptor.ClassTypeParameterList = SyntaxFactory.TypeParameterList();

Expand Down Expand Up @@ -182,8 +191,94 @@ x is IPropertySymbol xProperty &&
}
}

if (!member.Modifiers.Any(SyntaxKind.StaticKeyword))
var methodSymbol = memberSymbol as IMethodSymbol;
bool isImplicitExtension = false;

// For extension methods, determine the target type and handle parameters
if (methodSymbol is { IsExtensionMethod: true })
{
// Determine the extended type:
// - For traditional extensions (static): First parameter with 'this' modifier
// - For C# 14 extension blocks (non-static): Parameters include implicit extension parameter
ITypeSymbol? targetTypeSymbol = null;

// Detect C# 14 extension block: IsExtensionMethod but not static
// and containing type name starts with "extension"
bool isInExtensionBlock = !member.Modifiers.Any(SyntaxKind.StaticKeyword) &&
memberSymbol.ContainingType.Name.StartsWith("extension", StringComparison.Ordinal);
isImplicitExtension = isInExtensionBlock;

if (isInExtensionBlock)
{
// For C# 14 extension blocks, Roslyn 5.0.0 doesn't provide proper type information yet
// The method parameters reference the extension block type instead of the extended type
// Until Roslyn provides better support, C# 14 extension blocks cannot be fully supported
// For now, try using ReceiverType as a fallback
if (methodSymbol.ReceiverType is not null &&
!methodSymbol.ReceiverType.Name.StartsWith("extension", StringComparison.Ordinal))
{
// ReceiverType is available and not the extension block itself
targetTypeSymbol = methodSymbol.ReceiverType;
}
else if (methodSymbol.Parameters.Length > 0)
{
// Try first parameter, but it might be the extension block type
targetTypeSymbol = methodSymbol.Parameters[0].Type;
}
}
else
{
// Traditional extension method or ReceiverType available
if (methodSymbol.ReceiverType is not null)
{
targetTypeSymbol = methodSymbol.ReceiverType;
}
else if (methodSymbol.Parameters.Length > 0)
{
// Traditional extension method: get type from first parameter
targetTypeSymbol = methodSymbol.Parameters.First().Type;
}
}

if (targetTypeSymbol is null)
{
// Invalid extension method - couldn't determine target type
var diagnostic = Diagnostic.Create(
Diagnostics.RequiresExpressionBodyDefinition,
member.GetLocation(),
memberSymbol.Name
);
context.ReportDiagnostic(diagnostic);
return null;
}

descriptor.TargetClassNamespace = targetTypeSymbol.ContainingNamespace.IsGlobalNamespace ? null : targetTypeSymbol.ContainingNamespace.ToDisplayString();
descriptor.TargetNestedInClassNames = GetNestedInClassPath(targetTypeSymbol);

// For C# 14 extension blocks, we need to handle parameters specially
// The implicit extension parameter should be represented but not added to descriptor yet
// (it will be added from the method's parameter list)
// However, if the method declaration has no parameters (extension block implicit param),
// we need to add it synthetically
if (isImplicitExtension && methodSymbol.Parameters.Length > 0)
{
// Extension block methods have implicit parameters - add the first one as the receiver
var extensionParam = methodSymbol.Parameters[0];
descriptor.ParametersList = descriptor.ParametersList.AddParameters(
SyntaxFactory.Parameter(
SyntaxFactory.Identifier(extensionParam.Name)
)
.WithType(
SyntaxFactory.ParseTypeName(
targetTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
)
)
);
}
}
else if (!member.Modifiers.Any(SyntaxKind.StaticKeyword))
{
// Non-extension instance member
descriptor.ParametersList = descriptor.ParametersList.AddParameters(
SyntaxFactory.Parameter(
SyntaxFactory.Identifier("@this")
Expand All @@ -194,18 +289,13 @@ x is IPropertySymbol xProperty &&
)
)
);
}

var methodSymbol = memberSymbol as IMethodSymbol;

if (methodSymbol is { IsExtensionMethod: true })
{
var targetTypeSymbol = methodSymbol.Parameters.First().Type;
descriptor.TargetClassNamespace = targetTypeSymbol.ContainingNamespace.IsGlobalNamespace ? null : targetTypeSymbol.ContainingNamespace.ToDisplayString();
descriptor.TargetNestedInClassNames = GetNestedInClassPath(targetTypeSymbol);

descriptor.TargetClassNamespace = descriptor.ClassNamespace;
descriptor.TargetNestedInClassNames = descriptor.NestedInClassNames;
}
else
{
// Static non-extension member
descriptor.TargetClassNamespace = descriptor.ClassNamespace;
descriptor.TargetNestedInClassNames = descriptor.NestedInClassNames;
}
Expand All @@ -223,7 +313,13 @@ x is IPropertySymbol xProperty &&

descriptor.ReturnTypeName = returnType.ToString();
descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(methodDeclarationSyntax.ExpressionBody.Expression);
foreach (var additionalParameter in ((ParameterListSyntax)declarationSyntaxRewriter.Visit(methodDeclarationSyntax.ParameterList)).Parameters)

var parameters = ((ParameterListSyntax)declarationSyntaxRewriter.Visit(methodDeclarationSyntax.ParameterList)).Parameters;

// Add parameters from the method declaration
// For traditional extension methods, all parameters (including the 'this' parameter) are in the parameter list
// For C# 14 implicit extensions, the receiver is not in the parameter list (we added it above)
foreach (var additionalParameter in parameters)
{
descriptor.ParametersList = descriptor.ParametersList.AddParameters(additionalParameter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,68 @@ static class C {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}

[Fact(Skip = "C# 14 extension blocks not yet fully supported in Roslyn 5.0.0")]
public Task ProjectableCSharp14ExtensionMethod()
{
// C# 14 extension syntax with extension blocks
// Reference: https://devblogs.microsoft.com/dotnet/csharp-exploring-extension-members/
// Note: Roslyn 5.0.0 doesn't provide proper type information for extension blocks yet
var compilation = CreateCompilation(@"
using System;
using System.Linq;
using EntityFrameworkCore.Projectables;
namespace Foo {
public class D
{
public int Bar { get; set; }
}

public static class DExtensions {
extension(D d) {
[Projectable]
public int Foo() => d.Bar + 1;
}
}
}
");

var result = RunGenerator(compilation);

Assert.Empty(result.Diagnostics);
Assert.Single(result.GeneratedTrees);

return Verifier.Verify(result.GeneratedTrees[0].ToString());
}

[Fact(Skip = "C# 14 extension blocks not yet fully supported in Roslyn 5.0.0")]
public Task ProjectableCSharp14ExtensionMethodSimple()
{
// Simpler C# 14 extension test
// The extension block type information is not correctly exposed by Roslyn yet
var compilation = CreateCompilation(@"
using System;
using System.Linq;
using EntityFrameworkCore.Projectables;
namespace Foo {
class D { }

static class C {
extension(D d) {
[Projectable]
public int Foo() => 1;
}
}
}
");

var result = RunGenerator(compilation);

Assert.Empty(result.Diagnostics);
Assert.Single(result.GeneratedTrees);

return Verifier.Verify(result.GeneratedTrees[0].ToString());
}

[Fact]
public void BlockBodiedMember_RaisesDiagnostics()
{
Expand Down Expand Up @@ -1918,8 +1980,9 @@ Compilation CreateCompilation(string source, bool expectedToCompile = true)

references.Add(MetadataReference.CreateFromFile(typeof(ProjectableAttribute).Assembly.Location));

var parseOptions = new CSharpParseOptions(LanguageVersion.Preview); // Use Preview to enable C# 14 features
var compilation = CSharpCompilation.Create("compilation",
new[] { CSharpSyntaxTree.ParseText(source) },
new[] { CSharpSyntaxTree.ParseText(source, parseOptions) },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

Expand Down