diff --git a/Directory.Packages.props b/Directory.Packages.props index f668a8b..e859f56 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,10 +25,10 @@ + - diff --git a/EntityFrameworkCore.Projectables.sln b/EntityFrameworkCore.Projectables.sln index 7cf0767..a5f8bfa 100644 --- a/EntityFrameworkCore.Projectables.sln +++ b/EntityFrameworkCore.Projectables.sln @@ -53,44 +53,140 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\release.yml = .github\workflows\release.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Projectables.CodeFixes", "src\EntityFrameworkCore.Projectables.CodeFixes\EntityFrameworkCore.Projectables.CodeFixes.csproj", "{1890C6AF-37A4-40B0-BD0C-7FB18357891A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.Projectables.CodeFixes.Tests", "tests\EntityFrameworkCore.Projectables.CodeFixes.Tests\EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj", "{C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|Any CPU.Build.0 = Debug|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x64.ActiveCfg = Debug|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x64.Build.0 = Debug|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x86.ActiveCfg = Debug|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Debug|x86.Build.0 = Debug|Any CPU {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|Any CPU.ActiveCfg = Release|Any CPU {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|Any CPU.Build.0 = Release|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x64.ActiveCfg = Release|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x64.Build.0 = Release|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x86.ActiveCfg = Release|Any CPU + {698E3EEC-64F9-4F96-B700-D61D04FD0704}.Release|x86.Build.0 = Release|Any CPU {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x64.ActiveCfg = Debug|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x64.Build.0 = Debug|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x86.ActiveCfg = Debug|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Debug|x86.Build.0 = Debug|Any CPU {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|Any CPU.ActiveCfg = Release|Any CPU {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|Any CPU.Build.0 = Release|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x64.ActiveCfg = Release|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x64.Build.0 = Release|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x86.ActiveCfg = Release|Any CPU + {20F85652-2923-4211-9262-C64BA8C9ED89}.Release|x86.Build.0 = Release|Any CPU {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x64.ActiveCfg = Debug|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x64.Build.0 = Debug|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Debug|x86.Build.0 = Debug|Any CPU {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|Any CPU.Build.0 = Release|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x64.ActiveCfg = Release|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x64.Build.0 = Release|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x86.ActiveCfg = Release|Any CPU + {EE4D6CC1-78DE-4279-A567-C3D360C479F8}.Release|x86.Build.0 = Release|Any CPU {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x64.ActiveCfg = Debug|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x64.Build.0 = Debug|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x86.ActiveCfg = Debug|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Debug|x86.Build.0 = Debug|Any CPU {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|Any CPU.Build.0 = Release|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x64.ActiveCfg = Release|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x64.Build.0 = Release|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x86.ActiveCfg = Release|Any CPU + {C8038180-36F8-4077-922B-91F428EAC7D9}.Release|x86.Build.0 = Release|Any CPU {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x64.Build.0 = Debug|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Debug|x86.Build.0 = Debug|Any CPU {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|Any CPU.Build.0 = Release|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x64.ActiveCfg = Release|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x64.Build.0 = Release|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x86.ActiveCfg = Release|Any CPU + {2F0DD7D7-867F-4478-9E22-45C114B61C46}.Release|x86.Build.0 = Release|Any CPU {A36F5471-0C16-4453-811B-818326931313}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A36F5471-0C16-4453-811B-818326931313}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Debug|x64.ActiveCfg = Debug|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Debug|x64.Build.0 = Debug|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Debug|x86.ActiveCfg = Debug|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Debug|x86.Build.0 = Debug|Any CPU {A36F5471-0C16-4453-811B-818326931313}.Release|Any CPU.ActiveCfg = Release|Any CPU {A36F5471-0C16-4453-811B-818326931313}.Release|Any CPU.Build.0 = Release|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Release|x64.ActiveCfg = Release|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Release|x64.Build.0 = Release|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Release|x86.ActiveCfg = Release|Any CPU + {A36F5471-0C16-4453-811B-818326931313}.Release|x86.Build.0 = Release|Any CPU {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x64.Build.0 = Debug|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Debug|x86.Build.0 = Debug|Any CPU {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|Any CPU.Build.0 = Release|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x64.ActiveCfg = Release|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x64.Build.0 = Release|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x86.ActiveCfg = Release|Any CPU + {6F63E04C-9267-4211-8AC7-290C60331D60}.Release|x86.Build.0 = Release|Any CPU {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x64.Build.0 = Debug|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Debug|x86.Build.0 = Debug|Any CPU {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|Any CPU.Build.0 = Release|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x64.ActiveCfg = Release|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x64.Build.0 = Release|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x86.ActiveCfg = Release|Any CPU + {F2F01B61-5FB8-4F96-A6DE-824C9756B365}.Release|x86.Build.0 = Release|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x64.ActiveCfg = Debug|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x64.Build.0 = Debug|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Debug|x86.Build.0 = Debug|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|Any CPU.Build.0 = Release|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x64.ActiveCfg = Release|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x64.Build.0 = Release|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x86.ActiveCfg = Release|Any CPU + {1890C6AF-37A4-40B0-BD0C-7FB18357891A}.Release|x86.Build.0 = Release|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x64.ActiveCfg = Debug|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x64.Build.0 = Debug|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x86.ActiveCfg = Debug|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Debug|x86.Build.0 = Debug|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|Any CPU.Build.0 = Release|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x64.ActiveCfg = Release|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x64.Build.0 = Release|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x86.ActiveCfg = Release|Any CPU + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,6 +201,8 @@ Global {6F63E04C-9267-4211-8AC7-290C60331D60} = {07584D01-2D30-404B-B0D1-32080C0CC18A} {F2F01B61-5FB8-4F96-A6DE-824C9756B365} = {D1DB50EE-D639-46B0-8966-D0AA5F569620} {31596010-788E-434F-BF00-4EBC6E232822} = {C95A2C5D-4A3B-440C-A703-2D5892ABA7FE} + {1890C6AF-37A4-40B0-BD0C-7FB18357891A} = {A43F1828-D9B6-40F7-82B6-CA0070843E2F} + {C62B59E5-A32F-4CB5-ADB1-B3D03BBC8476} = {F5E4436F-87F2-46AB-A9EB-59B4BF21BF7A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D17BD356-592C-4628-9D81-A04E24FF02F3} diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj index c4c64eb..b7feab3 100644 --- a/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj +++ b/src/EntityFrameworkCore.Projectables.Abstractions/EntityFrameworkCore.Projectables.Abstractions.csproj @@ -11,11 +11,17 @@ Content PreserveNewest + + false + Content + PreserveNewest + - + + diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Shipped.md b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..c65238f --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ + + diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.CodeFixes/AnalyzerReleases.Unshipped.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/BlockBodyExperimentalCodeFixProvider.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/BlockBodyExperimentalCodeFixProvider.cs new file mode 100644 index 0000000..07ebd02 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.CodeFixes/BlockBodyExperimentalCodeFixProvider.cs @@ -0,0 +1,58 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace EntityFrameworkCore.Projectables.CodeFixes; + +/// +/// Code fix provider for EFP0001 (Block-bodied member support is experimental). +/// Adds AllowBlockBody = true to the [Projectable] attribute. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(BlockBodyExperimentalCodeFixProvider))] +[Shared] +public sealed class BlockBodyExperimentalCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ["EFP0001"]; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var diagnostic = context.Diagnostics[0]; + var node = root.FindNode(diagnostic.Location.SourceSpan); + var member = node.AncestorsAndSelf().OfType().FirstOrDefault(); + if (member is null) + { + return; + } + + if (!ProjectableCodeFixHelper.TryFindProjectableAttribute(member, out _)) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Add AllowBlockBody = true to [Projectable]", + createChangedDocument: ct => + ProjectableCodeFixHelper.AddOrReplaceNamedArgumentInProjectableAttributeAsync( + context.Document, + member, + "AllowBlockBody", + SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression), + ct), + equivalenceKey: "EFP0001_AddAllowBlockBody"), + diagnostic); + } +} + diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/EntityFrameworkCore.Projectables.CodeFixes.csproj b/src/EntityFrameworkCore.Projectables.CodeFixes/EntityFrameworkCore.Projectables.CodeFixes.csproj new file mode 100644 index 0000000..6f93b2e --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.CodeFixes/EntityFrameworkCore.Projectables.CodeFixes.csproj @@ -0,0 +1,15 @@ + + + + + netstandard2.0; + $(NoWarn);NU5128 + false + true + + + + + + + diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs new file mode 100644 index 0000000..7926bc4 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs @@ -0,0 +1,84 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; + +namespace EntityFrameworkCore.Projectables.CodeFixes; + +/// +/// Code fix provider for EFP0008 (Target class is missing a parameterless constructor). +/// Inserts a public ClassName() { } constructor into the class that carries the +/// [Projectable] constructor, satisfying the object-initializer requirement of the +/// generated expression tree. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingParameterlessConstructorCodeFixProvider))] +[Shared] +public sealed class MissingParameterlessConstructorCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ["EFP0008"]; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var diagnostic = context.Diagnostics[0]; + var node = root.FindNode(diagnostic.Location.SourceSpan); + + // The diagnostic is reported on the [Projectable] constructor declaration. + // Walk up to find the containing type. + var typeDecl = node.AncestorsAndSelf().OfType().FirstOrDefault(); + if (typeDecl is null) + { + return; + } + + var typeName = typeDecl.Identifier.Text; + + context.RegisterCodeFix( + CodeAction.Create( + title: $"Add parameterless constructor to '{typeName}'", + createChangedDocument: ct => AddParameterlessConstructorAsync(context.Document, typeDecl, ct), + equivalenceKey: "EFP0008_AddParameterlessConstructor"), + diagnostic); + } + + private async static Task AddParameterlessConstructorAsync( + Document document, + TypeDeclarationSyntax typeDecl, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + { + return document; + } + + var parameterlessCtor = SyntaxFactory + .ConstructorDeclaration(typeDecl.Identifier.WithoutTrivia()) + .WithModifiers(SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword) + .WithTrailingTrivia(SyntaxFactory.Space))) + .WithParameterList(SyntaxFactory.ParameterList()) + .WithBody(SyntaxFactory.Block()) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + + // Insert before the first existing member so it appears at the top of the class body. + var newTypeDecl = typeDecl.WithMembers( + typeDecl.Members.Insert(0, parameterlessCtor)); + + var newRoot = root.ReplaceNode(typeDecl, newTypeDecl); + return document.WithSyntaxRoot(newRoot); + } +} + diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/NullConditionalRewriteUnsupportedCodeFixProvider.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/NullConditionalRewriteUnsupportedCodeFixProvider.cs new file mode 100644 index 0000000..980867e --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.CodeFixes/NullConditionalRewriteUnsupportedCodeFixProvider.cs @@ -0,0 +1,84 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace EntityFrameworkCore.Projectables.CodeFixes; + +/// +/// Code fix provider for EFP0002 (Null-conditional expression unsupported). +/// Offers two options to configure NullConditionalRewriteSupport on the [Projectable] attribute: +/// +/// Ignore — strips the null-conditional operator from the generated expression tree. +/// Rewrite — translates the null-conditional operator into an explicit null check. +/// +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NullConditionalRewriteUnsupportedCodeFixProvider))] +[Shared] +public sealed class NullConditionalRewriteUnsupportedCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ["EFP0002"]; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var diagnostic = context.Diagnostics[0]; + var node = root.FindNode(diagnostic.Location.SourceSpan); + + // The diagnostic is on the null-conditional expression inside the body. + // Walk up to find the containing member that carries [Projectable]. + var member = node.AncestorsAndSelf() + .OfType() + .FirstOrDefault(m => ProjectableCodeFixHelper.TryFindProjectableAttribute(m, out _)); + + if (member is null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Set NullConditionalRewriteSupport = Ignore on [Projectable]", + createChangedDocument: ct => SetNullConditionalSupportAsync(context.Document, member, "Ignore", ct), + equivalenceKey: "EFP0002_Ignore"), + diagnostic); + + context.RegisterCodeFix( + CodeAction.Create( + title: "Set NullConditionalRewriteSupport = Rewrite on [Projectable]", + createChangedDocument: ct => SetNullConditionalSupportAsync(context.Document, member, "Rewrite", ct), + equivalenceKey: "EFP0002_Rewrite"), + diagnostic); + } + + private static Task SetNullConditionalSupportAsync( + Document document, + MemberDeclarationSyntax member, + string enumValueName, + CancellationToken cancellationToken) + { + // Produces: NullConditionalRewriteSupport.Ignore (or .Rewrite) + var enumValue = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("NullConditionalRewriteSupport"), + SyntaxFactory.IdentifierName(enumValueName)); + + return ProjectableCodeFixHelper.AddOrReplaceNamedArgumentInProjectableAttributeAsync( + document, + member, + "NullConditionalRewriteSupport", + enumValue, + cancellationToken); + } +} + diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/ProjectableCodeFixHelper.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/ProjectableCodeFixHelper.cs new file mode 100644 index 0000000..0821e64 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.CodeFixes/ProjectableCodeFixHelper.cs @@ -0,0 +1,76 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace EntityFrameworkCore.Projectables.CodeFixes; + +static internal class ProjectableCodeFixHelper +{ + private const string ProjectableAttributeName = "Projectable"; + private const string ProjectableAttributeFullName = "ProjectableAttribute"; + + static internal bool TryFindProjectableAttribute(MemberDeclarationSyntax member, out AttributeSyntax? attribute) + { + attribute = member.AttributeLists + .SelectMany(al => al.Attributes) + .FirstOrDefault(a => + { + var name = a.Name.ToString(); + return name == ProjectableAttributeName || name == ProjectableAttributeFullName; + }); + + return attribute is not null; + } + + /// + /// Adds or replaces a named argument on the [Projectable] attribute of . + /// If the attribute already has the argument, it is replaced; otherwise it is appended. + /// + async static internal Task AddOrReplaceNamedArgumentInProjectableAttributeAsync( + Document document, + MemberDeclarationSyntax member, + string argumentName, + ExpressionSyntax argumentValue, + CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + { + return document; + } + + if (!TryFindProjectableAttribute(member, out var attribute) || attribute is null) + { + return document; + } + + var newArgument = SyntaxFactory.AttributeArgument( + SyntaxFactory.NameEquals(SyntaxFactory.IdentifierName(argumentName)), + null, + argumentValue); + + AttributeSyntax newAttribute; + + if (attribute.ArgumentList is null) + { + newAttribute = attribute.WithArgumentList( + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList(newArgument))); + } + else + { + var existingArg = attribute.ArgumentList.Arguments + .FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == argumentName); + + newAttribute = existingArg is not null + ? attribute.WithArgumentList( + attribute.ArgumentList.ReplaceNode(existingArg, newArgument)) + : attribute.WithArgumentList( + attribute.ArgumentList.AddArguments(newArgument)); + } + + var newRoot = root.ReplaceNode(attribute, newAttribute); + return document.WithSyntaxRoot(newRoot); + } +} + diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_OnBlockBodiedProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_OnBlockBodiedProperty.verified.txt new file mode 100644 index 0000000..eb611e9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_OnBlockBodiedProperty.verified.txt @@ -0,0 +1,10 @@ + +namespace Foo { + class C { + [Projectable(AllowBlockBody = true)] + public int Double + { + get { return 2; } + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments.verified.txt new file mode 100644 index 0000000..0d99b03 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments.verified.txt @@ -0,0 +1,7 @@ + +namespace Foo { + class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore, AllowBlockBody = true)] + public int Bar() { return 42; } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableHasNoArguments.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableHasNoArguments.verified.txt new file mode 100644 index 0000000..b7ee2f6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.AddAllowBlockBody_WhenProjectableHasNoArguments.verified.txt @@ -0,0 +1,7 @@ + +namespace Foo { + class C { + [Projectable(AllowBlockBody = true)] + public int Bar() { return 42; } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.ReplaceAllowBlockBody_WhenAlreadySetToFalse.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.ReplaceAllowBlockBody_WhenAlreadySetToFalse.verified.txt new file mode 100644 index 0000000..b7ee2f6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.ReplaceAllowBlockBody_WhenAlreadySetToFalse.verified.txt @@ -0,0 +1,7 @@ + +namespace Foo { + class C { + [Projectable(AllowBlockBody = true)] + public int Bar() { return 42; } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.cs new file mode 100644 index 0000000..843aa22 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/BlockBodyExperimentalCodeFixProviderTests.cs @@ -0,0 +1,117 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.CodeFixes.Tests; + +/// +/// Tests for (EFP0001). +/// The fix adds AllowBlockBody = true to the [Projectable] attribute. +/// +[UsesVerify] +public class BlockBodyExperimentalCodeFixProviderTests : CodeFixTestBase +{ + private readonly static BlockBodyExperimentalCodeFixProvider _provider = new(); + + // Locates the first method identifier span — the code fix walks up to MemberDeclarationSyntax. + private static TextSpan FirstMethodIdentifierSpan(SyntaxNode root) => + root.DescendantNodes() + .OfType() + .First() + .Identifier + .Span; + + [Fact] + public void FixableDiagnosticIds_ContainsEFP0001() => + Assert.Contains("EFP0001", _provider.FixableDiagnosticIds); + + [Fact] + public Task AddAllowBlockBody_WhenProjectableHasNoArguments() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class C { + [Projectable] + public int Bar() { return 42; } + } +}", + "EFP0001", + FirstMethodIdentifierSpan, + _provider)); + + [Fact] + public Task AddAllowBlockBody_WhenProjectableAlreadyHasOtherArguments() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public int Bar() { return 42; } + } +}", + "EFP0001", + FirstMethodIdentifierSpan, + _provider)); + + [Fact] + public Task ReplaceAllowBlockBody_WhenAlreadySetToFalse() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class C { + [Projectable(AllowBlockBody = false)] + public int Bar() { return 42; } + } +}", + "EFP0001", + FirstMethodIdentifierSpan, + _provider)); + + [Fact] + public Task AddAllowBlockBody_OnBlockBodiedProperty() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class C { + [Projectable] + public int Double + { + get { return 2; } + } + } +}", + "EFP0001", + root => root.DescendantNodes() + .OfType() + .First() + .Identifier + .Span, + _provider)); + + [Fact] + public async Task NoCodeFix_WhenMemberHasNoProjectableAttribute() + { + var actions = await GetCodeFixActionsAsync( + @" +namespace Foo { + class C { + [OtherAttribute] + public int Bar() { return 42; } + } +}", + "EFP0001", + FirstMethodIdentifierSpan, + _provider); + + Assert.Empty(actions); + } +} + + + diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/CodeFixTestBase.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/CodeFixTestBase.cs new file mode 100644 index 0000000..9bc523b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/CodeFixTestBase.cs @@ -0,0 +1,109 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace EntityFrameworkCore.Projectables.CodeFixes.Tests; + +/// +/// Base class providing helpers for code fix provider tests. +/// Creates an in-memory workspace document, builds a synthetic diagnostic at the +/// supplied span, invokes the provider, and optionally applies a code action. +/// +public abstract class CodeFixTestBase +{ + private static Document CreateDocument(string source) + { + var workspace = new AdhocWorkspace(); + var projectId = ProjectId.CreateNewId(); + var documentId = DocumentId.CreateNewId(projectId); + + var solution = workspace.CurrentSolution + .AddProject(projectId, "TestProject", "TestProject", LanguageNames.CSharp) + .WithProjectCompilationOptions( + projectId, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddDocument(documentId, "Test.cs", SourceText.From(source)); + + return solution.GetDocument(documentId)!; + } + + private async static Task<(Document Document, IReadOnlyList Actions)> CollectActionsAsync( + string source, + string diagnosticId, + Func locateDiagnosticSpan, + CodeFixProvider provider) + { + var document = CreateDocument(source); + var root = await document.GetSyntaxRootAsync(); + var span = locateDiagnosticSpan(root!); + + var tree = await document.GetSyntaxTreeAsync(); + var descriptor = new DiagnosticDescriptor( + diagnosticId, + "Test diagnostic", + "Test message", + "Test", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + var location = Location.Create(tree!, span); + var diagnostic = Diagnostic.Create(descriptor, location); + + var actions = new List(); + var context = new CodeFixContext( + document, + diagnostic, + (action, _) => actions.Add(action), + CancellationToken.None); + + await provider.RegisterCodeFixesAsync(context); + return (document, actions); + } + + /// + /// Collects the titles offered by + /// for a synthetic diagnostic with located at the span + /// returned by . + /// + protected async static Task> GetCodeFixActionsAsync( + string source, + string diagnosticId, + Func locateDiagnosticSpan, + CodeFixProvider provider) + { + var (_, actions) = await CollectActionsAsync(source, diagnosticId, locateDiagnosticSpan, provider); + return actions; + } + + /// + /// Applies the code fix action at and returns the full + /// source text of the resulting document. + /// + protected async static Task ApplyCodeFixAsync( + [StringSyntax("csharp")] + string source, + string diagnosticId, + Func locateDiagnosticSpan, + CodeFixProvider provider, + int actionIndex = 0) + { + var (document, actions) = await CollectActionsAsync(source, diagnosticId, locateDiagnosticSpan, provider); + + Assert.True( + actions.Count > actionIndex, + $"Expected at least {actionIndex + 1} code fix action(s) but only {actions.Count} were registered."); + + var action = actions[actionIndex]; + var operations = await action.GetOperationsAsync(CancellationToken.None); + var applyOp = operations.OfType().Single(); + + var newDocument = applyOp.ChangedSolution.GetDocument(document.Id)!; + var newRoot = await newDocument.GetSyntaxRootAsync(); + return newRoot!.ToFullString(); + } +} + diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj new file mode 100644 index 0000000..5771aea --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/EntityFrameworkCore.Projectables.CodeFixes.Tests.csproj @@ -0,0 +1,28 @@ + + + + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_IsInsertedBeforeExistingMembers.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_IsInsertedBeforeExistingMembers.verified.txt new file mode 100644 index 0000000..017971e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_IsInsertedBeforeExistingMembers.verified.txt @@ -0,0 +1,18 @@ + +namespace Foo { + class Person { + public Person() + { + } + + public string Name { get; set; } + public int Age { get; set; } + + [Projectable] + public Person(string name, int age) + { + Name = name; + Age = age; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithNoOtherMembers.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithNoOtherMembers.verified.txt new file mode 100644 index 0000000..68aecfd --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithNoOtherMembers.verified.txt @@ -0,0 +1,11 @@ + +namespace Foo { + class Empty { + public Empty() + { + } + + [Projectable] + public Empty(int value) { } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithSingleParamConstructor.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithSingleParamConstructor.verified.txt new file mode 100644 index 0000000..d6f2a21 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddParamLessConstructor_ToClassWithSingleParamConstructor.verified.txt @@ -0,0 +1,16 @@ + +namespace Foo { + class Person { + public Person() + { + } + + [Projectable] + public Person(string name) + { + Name = name; + } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParameterlessConstructorCodeFixProviderTests.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParameterlessConstructorCodeFixProviderTests.cs new file mode 100644 index 0000000..d90bcbe --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParameterlessConstructorCodeFixProviderTests.cs @@ -0,0 +1,107 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.CodeFixes.Tests; + +/// +/// Tests for (EFP0008). +/// The fix inserts a public ClassName() { } constructor at the top of the class body. +/// +[UsesVerify] +public class MissingParamLessConstructorCodeFixProviderTests : CodeFixTestBase +{ + private readonly static MissingParameterlessConstructorCodeFixProvider _provider = new(); + + // Locates the first constructor identifier — the code fix walks up to TypeDeclarationSyntax. + private static TextSpan FirstConstructorIdentifierSpan(SyntaxNode root) => + root.DescendantNodes() + .OfType() + .First() + .Identifier + .Span; + + [Fact] + public void FixableDiagnosticIds_ContainsEFP0008() => + Assert.Contains("EFP0008", _provider.FixableDiagnosticIds); + + [Fact] + public async Task RegistersCodeFix_WithTitleContainingClassName() + { + var actions = await GetCodeFixActionsAsync( + @" +namespace Foo { + class MyClass { + [Projectable] + public MyClass(int value) { } + } +}", + "EFP0008", + FirstConstructorIdentifierSpan, + _provider); + + var action = Assert.Single(actions); + Assert.Contains("MyClass", action.Title, StringComparison.Ordinal); + } + + [Fact] + public Task AddParamLessConstructor_ToClassWithSingleParamConstructor() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class Person { + [Projectable] + public Person(string name) + { + Name = name; + } + + public string Name { get; set; } + } +}", + "EFP0008", + FirstConstructorIdentifierSpan, + _provider)); + + [Fact] + public Task AddParamLessConstructor_IsInsertedBeforeExistingMembers() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class Person { + public string Name { get; set; } + public int Age { get; set; } + + [Projectable] + public Person(string name, int age) + { + Name = name; + Age = age; + } + } +}", + "EFP0008", + FirstConstructorIdentifierSpan, + _provider)); + + [Fact] + public Task AddParamLessConstructor_ToClassWithNoOtherMembers() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class Empty { + [Projectable] + public Empty(int value) { } + } +}", + "EFP0008", + FirstConstructorIdentifierSpan, + _provider)); +} + + diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithIgnore.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithIgnore.verified.txt new file mode 100644 index 0000000..e53ae15 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithIgnore.verified.txt @@ -0,0 +1,9 @@ + +namespace Foo { + class C { + public string Name { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public int NameLength() => Name?.Length ?? 0; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithRewrite.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithRewrite.verified.txt new file mode 100644 index 0000000..4186e98 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.ReplacesExistingNullConditionalRewriteSupport_WithRewrite.verified.txt @@ -0,0 +1,9 @@ + +namespace Foo { + class C { + public string Name { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public int NameLength() => Name?.Length ?? 0; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Ignore.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Ignore.verified.txt new file mode 100644 index 0000000..e53ae15 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Ignore.verified.txt @@ -0,0 +1,9 @@ + +namespace Foo { + class C { + public string Name { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public int NameLength() => Name?.Length ?? 0; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Rewrite.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Rewrite.verified.txt new file mode 100644 index 0000000..4186e98 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.SetNullConditionalSupport_Rewrite.verified.txt @@ -0,0 +1,9 @@ + +namespace Foo { + class C { + public string Name { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public int NameLength() => Name?.Length ?? 0; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.cs new file mode 100644 index 0000000..36f4d22 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/NullConditionalRewriteUnsupportedCodeFixProviderTests.cs @@ -0,0 +1,130 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.CodeFixes.Tests; + +/// +/// Tests for (EFP0002). +/// The fix sets NullConditionalRewriteSupport on the [Projectable] attribute to +/// either Ignore (action index 0) or Rewrite (action index 1). +/// +[UsesVerify] +public class NullConditionalRewriteUnsupportedCodeFixProviderTests : CodeFixTestBase +{ + private readonly static NullConditionalRewriteUnsupportedCodeFixProvider _provider = new(); + + private const string SourceWithNullConditional = @" +namespace Foo { + class C { + public string Name { get; set; } + + [Projectable] + public int NameLength() => Name?.Length ?? 0; + } +}"; + + // Locates the first null-conditional expression — the code fix walks up to a [Projectable] member. + private static TextSpan FirstConditionalAccessSpan(SyntaxNode root) => + root.DescendantNodes() + .OfType() + .First() + .Span; + + [Fact] + public void FixableDiagnosticIds_ContainsEFP0002() => + Assert.Contains("EFP0002", _provider.FixableDiagnosticIds); + + [Fact] + public async Task RegistersBothCodeFixes() + { + var actions = await GetCodeFixActionsAsync( + SourceWithNullConditional, + "EFP0002", + FirstConditionalAccessSpan, + _provider); + + Assert.Equal(2, actions.Count); + Assert.Contains(actions, a => a.Title.Contains("Ignore", StringComparison.Ordinal)); + Assert.Contains(actions, a => a.Title.Contains("Rewrite", StringComparison.Ordinal)); + } + + [Fact] + public Task SetNullConditionalSupport_Ignore() => + Verifier.Verify( + ApplyCodeFixAsync( + SourceWithNullConditional, + "EFP0002", + FirstConditionalAccessSpan, + _provider, + actionIndex: 0)); + + [Fact] + public Task SetNullConditionalSupport_Rewrite() => + Verifier.Verify( + ApplyCodeFixAsync( + SourceWithNullConditional, + "EFP0002", + FirstConditionalAccessSpan, + _provider, + actionIndex: 1)); + + [Fact] + public Task ReplacesExistingNullConditionalRewriteSupport_WithRewrite() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class C { + public string Name { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public int NameLength() => Name?.Length ?? 0; + } +}", + "EFP0002", + FirstConditionalAccessSpan, + _provider, + actionIndex: 1)); + + [Fact] + public Task ReplacesExistingNullConditionalRewriteSupport_WithIgnore() => + Verifier.Verify( + ApplyCodeFixAsync( + @" +namespace Foo { + class C { + public string Name { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public int NameLength() => Name?.Length ?? 0; + } +}", + "EFP0002", + FirstConditionalAccessSpan, + _provider, + actionIndex: 0)); + + [Fact] + public async Task NoCodeFix_WhenContainingMemberHasNoProjectableAttribute() + { + var actions = await GetCodeFixActionsAsync( + @" +namespace Foo { + class C { + public string Name { get; set; } + + public int NameLength() => Name?.Length ?? 0; + } +}", + "EFP0002", + FirstConditionalAccessSpan, + _provider); + + Assert.Empty(actions); + } +} + + diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/VerifyInit.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/VerifyInit.cs new file mode 100644 index 0000000..b56ac82 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/VerifyInit.cs @@ -0,0 +1,25 @@ +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace EntityFrameworkCore.Projectables.CodeFixes.Tests; + +public static class VerifyInit +{ + [ModuleInitializer] + public static void Initialize() + { + // Auto-accept new/changed snapshots only when explicitly requested. + // Set VERIFY_AUTO_APPROVE=true when adding new tests to generate initial .verified.txt files. + // Do NOT set this for normal runs — snapshot mismatches must be visible as test failures. + if (Environment.GetEnvironmentVariable("VERIFY_AUTO_APPROVE") == "true") + { + VerifierSettings.AutoVerify(); + } + + // Force English culture so that snapshot output is consistent + // regardless of the developer's OS locale. + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); + CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); + } +} + diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj index cd9ad0e..a43b03b 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj @@ -13,7 +13,6 @@ - @@ -23,6 +22,7 @@ + diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs index d278e41..d6032e5 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs @@ -6,7 +6,6 @@ using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs index 524c17d..1bd9866 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/InheritedModelTests.cs @@ -8,7 +8,6 @@ using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using EntityFrameworkCore.Projectables.Services; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs index e9299df..86d5970 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullComplexFunctionTests.cs @@ -7,7 +7,6 @@ using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using EntityFrameworkCore.Projectables.Services; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs index a2dadc2..81a0a60 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullPropertyTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs index 2e2cd22..0f4c7e3 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatefullSimpleFunctionTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs index b034c70..a1bb9ac 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessComplexFunctionTests.cs @@ -7,7 +7,6 @@ using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using EntityFrameworkCore.Projectables.Services; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs index 1a76122..763e2ec 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StatelessSimpleFunctionTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs index 74e10b4..f89e096 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs @@ -7,7 +7,6 @@ using EntityFrameworkCore.Projectables.Extensions; using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs index 3b62b72..f685128 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/UseMemberBodyPropertyTests.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; -using ScenarioTests; using VerifyXunit; using Xunit;