diff --git a/README.md b/README.md index 88ac302..4186680 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ PM> Install-Package M31.FluentApi A package reference will be added to your `csproj` file. Moreover, since this library provides code via source code generation, consumers of your project don't need the reference to `M31.FluentApi`. Therefore, it is recommended to use the `PrivateAssets` metadata tag: ```xml - + ``` If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file: diff --git a/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md b/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md index f0df066..7b4d393 100644 --- a/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md +++ b/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md @@ -50,4 +50,13 @@ M31FA018 | M31.Usage | Error | Generic types are not supported Rule ID | Category | Severity | Notes --------|----------|----------|------- M31FA021 | M31.Usage | Error | Reserved method name -M31FA022 | M31.Usage | Error | Fluent lambda member without Fluent API \ No newline at end of file +M31FA022 | M31.Usage | Error | Fluent lambda member without Fluent API + + +## Release 1.5.0 + +### Removed Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +M31FA007 | M31.Usage | Error | Partial types are not supported \ No newline at end of file diff --git a/src/M31.FluentApi.Generator/Commons/SyntaxNodeExtensions.cs b/src/M31.FluentApi.Generator/Commons/SyntaxNodeExtensions.cs index 3fd582f..d234a00 100644 --- a/src/M31.FluentApi.Generator/Commons/SyntaxNodeExtensions.cs +++ b/src/M31.FluentApi.Generator/Commons/SyntaxNodeExtensions.cs @@ -5,10 +5,10 @@ namespace M31.FluentApi.Generator.Commons; internal static class SyntaxNodeExtensions { - internal static bool IsTypeDeclarationOfInterest( + internal static bool IsClassStructOrRecordSyntax( this SyntaxNode? syntaxNode, out TypeDeclarationSyntax typeDeclaration) { - if (syntaxNode.IsTypeDeclarationOfInterest()) + if (syntaxNode.IsClassStructOrRecordSyntax()) { typeDeclaration = (TypeDeclarationSyntax)syntaxNode!; return true; @@ -18,8 +18,26 @@ internal static bool IsTypeDeclarationOfInterest( return false; } - internal static bool IsTypeDeclarationOfInterest(this SyntaxNode? syntaxNode) + internal static bool IsClassStructOrRecordSyntax(this SyntaxNode? syntaxNode) { return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax or RecordDeclarationSyntax; } + + internal static bool IsFluentApiAttributeSyntax(this AttributeSyntax attributeSyntax) + { + string? name = ExtractName(attributeSyntax.Name); + + // Note that we drop alias support for better performance. + return name is "FluentApi" or "FluentApiAttribute"; + + string? ExtractName(NameSyntax nameSyntax) + { + return nameSyntax switch + { + SimpleNameSyntax simpleNameSyntax => simpleNameSyntax.Identifier.Text, // without namespace + QualifiedNameSyntax qualifiedNameSyntax => qualifiedNameSyntax.Right.Identifier.Text, // fully qualified + _ => null + }; + } + } } \ No newline at end of file diff --git a/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj b/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj index 17dfe6c..1f28998 100644 --- a/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj +++ b/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj @@ -11,7 +11,7 @@ true true true - 1.4.0 + 1.5.0 Kevin Schaal The generator package for M31.FluentAPI. Don't install this package explicitly, install M31.FluentAPI instead. fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration diff --git a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs index 2bd4182..21f528b 100644 --- a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs +++ b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs @@ -20,7 +20,12 @@ public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration, SyntaxKind.StructDeclaration); + context.RegisterSyntaxNodeAction( + AnalyzeNode, + SyntaxKind.ClassDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.RecordDeclaration, + SyntaxKind.RecordStructDeclaration); } private void AnalyzeNode(SyntaxNodeAnalysisContext context) @@ -51,21 +56,7 @@ private void AnalyzeNodeInternal(SyntaxNodeAnalysisContext context) return; } - ImmutableArray syntaxReferences = symbol.DeclaringSyntaxReferences; - - if (syntaxReferences.Length == 0) - { - return; - } - - SyntaxNode syntaxNode = syntaxReferences.First().GetSyntax(); - - if (!syntaxNode.IsTypeDeclarationOfInterest(out TypeDeclarationSyntax typeDeclaration)) - { - return; - } - - if (ReportErrorDiagnosticForPartialKeyword(context, typeDeclaration, symbol)) + if (context.Node is not TypeDeclarationSyntax typeDeclaration) { return; } @@ -92,23 +83,4 @@ private void AnalyzeNodeInternal(SyntaxNodeAnalysisContext context) context.ReportDiagnostic(diagnostic); } } - - private bool ReportErrorDiagnosticForPartialKeyword( - SyntaxNodeAnalysisContext context, - TypeDeclarationSyntax typeDeclaration, - INamedTypeSymbol symbol) - { - SyntaxToken partialKeyword = typeDeclaration.Modifiers.FirstOrDefault( - m => m.IsKind(SyntaxKind.PartialKeyword)); - - if (partialKeyword != default) - { - context.ReportDiagnostic(UnsupportedPartialType.CreateDiagnostic( - partialKeyword, - symbol.Name)); - return true; - } - - return false; - } } \ No newline at end of file diff --git a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs index 6d56185..ad1edb0 100644 --- a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs +++ b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs @@ -18,7 +18,6 @@ internal static class FluentApiDiagnostics DuplicateAttribute.Descriptor, OrthogonalAttributeMisused.Descriptor, DuplicateMainAttribute.Descriptor, - UnsupportedPartialType.Descriptor, InvalidFluentPredicateType.Descriptor, InvalidFluentNullableType.Descriptor, FluentNullableTypeWithoutNullableAnnotation.Descriptor, @@ -144,22 +143,6 @@ internal static Diagnostic CreateDiagnostic(AttributeDataExtended attributeData) } } - internal static class UnsupportedPartialType - { - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: "M31FA007", - title: "Partial types are not supported", - messageFormat: "Partial types are not supported. Remove partial keyword from type '{0}'.", - category: "M31.Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - internal static Diagnostic CreateDiagnostic(SyntaxToken partialKeyword, string typeName) - { - return Diagnostic.Create(Descriptor, partialKeyword.GetLocation(), typeName); - } - } - internal static class InvalidFluentPredicateType { internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( diff --git a/src/M31.FluentApi.Generator/SourceGenerators/SourceGenerator.cs b/src/M31.FluentApi.Generator/SourceGenerators/SourceGenerator.cs index 2ab89c0..2159892 100644 --- a/src/M31.FluentApi.Generator/SourceGenerators/SourceGenerator.cs +++ b/src/M31.FluentApi.Generator/SourceGenerators/SourceGenerator.cs @@ -16,7 +16,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var infos = context.SyntaxProvider .CreateSyntaxProvider(CanBeFluentApiClass, GetFluentApiClassInfo) .Where(info => info is not null) - .Collect() // Handle partial classes, get an array of related info objects into GenerateCode. + .Collect() // Handle partial classes, get an array of related info objects. .SelectMany((infos, _) => infos.Distinct()); // Want to have every fluent API info object only once. @@ -73,7 +73,7 @@ private void SourceOutputAction(SourceProductionContext ctx, FluentApiClassInfo { SyntaxNode? syntaxNode = ctx.Node.Parent?.Parent; - if (!syntaxNode.IsTypeDeclarationOfInterest(out TypeDeclarationSyntax typeDeclaration)) + if (!syntaxNode.IsClassStructOrRecordSyntax(out TypeDeclarationSyntax typeDeclaration)) { return null; } @@ -106,22 +106,11 @@ private bool CanBeFluentApiClass(SyntaxNode node, CancellationToken cancellation // The parent of the attribute is a list of attributes, the parent of the parent is the class. SyntaxNode? syntaxNode = attributeSyntax.Parent?.Parent; - if (!syntaxNode.IsTypeDeclarationOfInterest()) + if (!syntaxNode.IsClassStructOrRecordSyntax()) { return false; } - string? name = ExtractName(attributeSyntax.Name); - return name is "FluentApi" or "FluentApiAttribute"; - } - - private string? ExtractName(NameSyntax nameSyntax) - { - return nameSyntax switch - { - SimpleNameSyntax simpleNameSyntax => simpleNameSyntax.Identifier.Text, // without namespace - QualifiedNameSyntax qualifiedNameSyntax => qualifiedNameSyntax.Right.Identifier.Text, // fully qualified - _ => null - }; + return attributeSyntax.IsFluentApiAttributeSyntax(); } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs index 7376b79..1562fdd 100644 --- a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using M31.FluentApi.Tests.AnalyzerAndCodeFixes.Helpers; +using Microsoft.CodeAnalysis.Testing; using Xunit; using static M31.FluentApi.Tests.AnalyzerAndCodeFixes.Helpers.TestSourceCodeReader; using static M31.FluentApi.Generator.SourceAnalyzers.FluentApiDiagnostics; @@ -13,7 +15,7 @@ public class AnalyzerAndCodeFixTests [Fact] public async Task CanDetectConflictingControlAttributes1() { - (string source, string fixedSource) = ReadSource("ConflictingControlAttributesClass1", "Student"); + SourceWithFix source = ReadSource("ConflictingControlAttributesClass1", "Student"); var expectedDiagnostic1 = Verifier.Diagnostic(ConflictingControlAttributes.Descriptor.Id) .WithLocation(13, 6); @@ -21,13 +23,13 @@ public async Task CanDetectConflictingControlAttributes1() var expectedDiagnostic2 = Verifier.Diagnostic(ConflictingControlAttributes.Descriptor.Id) .WithLocation(14, 6); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic1, expectedDiagnostic2); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic1, expectedDiagnostic2); } [Fact] public async Task CanDetectConflictingControlAttributes2() { - (string source, string fixedSource) = ReadSource("ConflictingControlAttributesClass2", "Student"); + SourceWithFix source = ReadSource("ConflictingControlAttributesClass2", "Student"); var expectedDiagnostic1 = Verifier.Diagnostic(ConflictingControlAttributes.Descriptor.Id) .WithLocation(13, 6); @@ -35,13 +37,13 @@ public async Task CanDetectConflictingControlAttributes2() var expectedDiagnostic2 = Verifier.Diagnostic(ConflictingControlAttributes.Descriptor.Id) .WithLocation(17, 6); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic1, expectedDiagnostic2); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic1, expectedDiagnostic2); } [Fact] public async Task CanDetectConflictingControlAttributes3() { - (string source, string fixedSource) = ReadSource("ConflictingControlAttributesClass3", "Student"); + SourceWithFix source = ReadSource("ConflictingControlAttributesClass3", "Student"); var expectedDiagnostic1 = Verifier.Diagnostic(ConflictingControlAttributes.Descriptor.Id) .WithLocation(16, 6); @@ -49,185 +51,223 @@ public async Task CanDetectConflictingControlAttributes3() var expectedDiagnostic2 = Verifier.Diagnostic(ConflictingControlAttributes.Descriptor.Id) .WithLocation(17, 6); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic1, expectedDiagnostic2); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic1, expectedDiagnostic2); } [Fact] public async Task CanDetectFluentLambdaMemberWithoutFluentApiClass() { - (string source, _) = ReadSource("FluentLambdaMemberWithoutFluentApiClass", "Student"); + SourceWithFix source = ReadSource("FluentLambdaMemberWithoutFluentApiClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(FluentLambdaMemberWithoutFluentApi.Descriptor.Id) .WithLocation(14, 6) .WithArguments("Address"); - await Verifier.VerifyCodeFixAsync(source, null, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectDuplicateMainAttribute() { - (string source, string fixedSource) = ReadSource("DuplicateMainAttributeClass", "Student"); + SourceWithFix source = ReadSource("DuplicateMainAttributeClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(DuplicateMainAttribute.Descriptor.Id) .WithLocation(11, 6); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectGetMissingSetAndAddSetAccessor() { - (string source, string fixedSource) = ReadSource("GetMissingSetClass", "Student"); + SourceWithFix source = ReadSource("GetMissingSetClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(MissingSetAccessor.Descriptor.Id) .WithLocation(11, 9) .WithArguments("Semester"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); + } + + [Fact] + public async Task CanDetectGetMissingSetAndAddSetAccessorForRecords() + { + SourceWithFix source = ReadSource("GetMissingSetRecord", "Student"); + + var expectedDiagnostic = Verifier.Diagnostic(MissingSetAccessor.Descriptor.Id) + .WithLocation(11, 9) + .WithArguments("Semester"); + + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); + } + + [Fact] + public async Task CanDetectGetMissingSetAndAddSetAccessorForRecordStructs() + { + SourceWithFix source = ReadSource("GetMissingSetRecordStruct", "Student"); + + var expectedDiagnostic = Verifier.Diagnostic(MissingSetAccessor.Descriptor.Id) + .WithLocation(11, 9) + .WithArguments("Semester"); + + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); + } + + [Fact] + public async Task CanDetectGetMissingSetAndAddSetAccessorForStructs() + { + SourceWithFix source = ReadSource("GetMissingSetStruct", "Student"); + + var expectedDiagnostic = Verifier.Diagnostic(MissingSetAccessor.Descriptor.Id) + .WithLocation(11, 9) + .WithArguments("Semester"); + + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectInvalidCollectionType() { - (string source, string fixedSource) = ReadSource("InvalidCollectionTypeClass", "Student"); + SourceWithFix source = ReadSource("InvalidCollectionTypeClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(UnsupportedFluentCollectionType.Descriptor.Id) .WithLocation(13, 12) .WithArguments("string"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectInvalidFluentMethodReturnType() { - (string source, _) = ReadSource("InvalidFluentMethodReturnTypeClass", "Student"); + SourceWithFix source = ReadSource("InvalidFluentMethodReturnTypeClass", "Student"); + + // Pass null for the fixed source because it does not compile. See also file Student.fixed.illustration.txt. + source = source with { FixedSource = null }; var expectedDiagnostic = Verifier.Diagnostic(InvalidFluentMethodReturnType.Descriptor.Id) .WithLocation(14, 12) .WithArguments("int"); - await Verifier.VerifyCodeFixAsync(source, null, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectInvalidFluentPredicateType() { - (string source, string fixedSource) = ReadSource("InvalidFluentPredicateTypeClass", "Student"); + SourceWithFix source = ReadSource("InvalidFluentPredicateTypeClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(InvalidFluentPredicateType.Descriptor.Id) .WithLocation(13, 12) .WithArguments("string"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectInvalidNullableType() { - (string source, string fixedSource) = ReadSource("InvalidNullableTypeClass", "Student"); + SourceWithFix source = ReadSource("InvalidNullableTypeClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(InvalidFluentNullableType.Descriptor.Id) .WithLocation(10, 12) .WithArguments("int"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectMissingBuilderStep() { - (string source, string fixedSource) = ReadSource("MissingBuilderStepClass", "Student"); + SourceWithFix source = ReadSource("MissingBuilderStepClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(MissingBuilderStep.Descriptor.Id) .WithLocation(12, 6) .WithArguments(99); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectMissingDefaultConstructor() { - (string source, string fixedSource) = ReadSource("MissingDefaultConstructorClass", "Student"); + SourceWithFix source = ReadSource("MissingDefaultConstructorClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(MissingDefaultConstructor.Descriptor.Id) .WithLocation(6, 14) .WithArguments("Student"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectNullableTypeNoNullableAnnotation() { - (string source, string fixedSource) = ReadSource("NullableTypeNoNullableAnnotationClass", "Student"); + SourceWithFix source = ReadSource("NullableTypeNoNullableAnnotationClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(FluentNullableTypeWithoutNullableAnnotation.Descriptor.Id) .WithLocation(13, 12) .WithArguments("string"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectOrthogonalAttributeInCompound() { - (string source, string fixedSource) = ReadSource("OrthogonalAttributeInCompoundClass", "Student"); + SourceWithFix source = ReadSource("OrthogonalAttributeInCompoundClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(OrthogonalAttributeMisusedWithCompound.Descriptor.Id) .WithLocation(16, 6) .WithArguments("FluentDefault"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectOrthogonalAttributeWithoutMainAttribute() { - (string source, string fixedSource) = ReadSource("OrthogonalAttributeWithoutMainAttributeClass", "Student"); + SourceWithFix source = ReadSource("OrthogonalAttributeWithoutMainAttributeClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(OrthogonalAttributeMisused.Descriptor.Id) .WithLocation(10, 6) .WithArguments("FluentDefault"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] - public async Task CanDetectPartialClass() + public async Task CanHandlePartialClass() { - (string source, string fixedSource) = ReadSource("PartialClass", "Student"); - - var expectedDiagnostic = Verifier.Diagnostic(UnsupportedPartialType.Descriptor.Id) - .WithLocation(8, 8) - .WithArguments("Student"); + SourceWithFix source1 = ReadSource("PartialClass", "Student1"); + SourceWithFix source2 = ReadSource("PartialClass", "Student2"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync( + new SourceWithFix[] { source1, source2 }, + DiagnosticResult.EmptyDiagnosticResults); } [Fact] public async Task CanDetectPrivateGetMissingSetAndAddSetAccessor() { - (string source, string fixedSource) = ReadSource("PrivateGetMissingSetClass", "Student"); + SourceWithFix source = ReadSource("PrivateGetMissingSetClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(MissingSetAccessor.Descriptor.Id) .WithLocation(11, 17) .WithArguments("Semester"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } [Fact] public async Task CanDetectPublicGetMissingSetAndAddPrivateSetAccessor() { - (string source, string fixedSource) = ReadSource("PublicGetMissingSetClass", "Student"); + SourceWithFix source = ReadSource("PublicGetMissingSetClass", "Student"); var expectedDiagnostic = Verifier.Diagnostic(MissingSetAccessor.Descriptor.Id) .WithLocation(11, 16) .WithArguments("Semester"); - await Verifier.VerifyCodeFixAsync(source, fixedSource, expectedDiagnostic); + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/DiagnosticsDuringGenerationTests.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/DiagnosticsDuringGenerationTests.cs index 4a59789..cc14b87 100644 --- a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/DiagnosticsDuringGenerationTests.cs +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/DiagnosticsDuringGenerationTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using M31.FluentApi.Tests.AnalyzerAndCodeFixes.Helpers; using M31.FluentApi.Tests.Helpers; using Microsoft.CodeAnalysis; using Xunit; @@ -17,7 +18,7 @@ public void CanDetectDuplicateMethod1() "WithName", (11, 6), (14, 6)); - RunGeneratorAndCheckDiagnostics("DuplicateMethodClass1", expectedDiagnostic); + RunGeneratorAndCheckDiagnostics("DuplicateMethodClass1", "Student", expectedDiagnostic); } [Fact] @@ -28,7 +29,7 @@ public void CanDetectDuplicateMethod2() "WithFriend", (12, 6), (15, 6)); - RunGeneratorAndCheckDiagnostics("DuplicateMethodClass2", expectedDiagnostic); + RunGeneratorAndCheckDiagnostics("DuplicateMethodClass2", "Student", expectedDiagnostic); } [Fact] @@ -39,7 +40,7 @@ public void CanDetectDuplicateMethod3() "WithName", (14, 6), (20, 6)); - RunGeneratorAndCheckDiagnostics("DuplicateMethodClass3", expectedDiagnostic); + RunGeneratorAndCheckDiagnostics("DuplicateMethodClass3", "Student", expectedDiagnostic); } [Fact] @@ -50,7 +51,7 @@ public void CanDetectDuplicateMethod4() "Method1", (12, 6), (17, 6), (22, 6)); - RunGeneratorAndCheckDiagnostics("DuplicateMethodClass4", expectedDiagnostic); + RunGeneratorAndCheckDiagnostics("DuplicateMethodClass4", "Student", expectedDiagnostic); } [Fact] @@ -61,7 +62,17 @@ public void CanDetectDuplicateMethod5() "Method1", (12, 6), (17, 6)); - RunGeneratorAndCheckDiagnostics("DuplicateMethodClass5", expectedDiagnostic); + RunGeneratorAndCheckDiagnostics("DuplicateMethodClass5", "Student", expectedDiagnostic); + } + + [Fact] + public void CanDetectDuplicateMethodPartialClass() + { + ExpectedDiagnostic expectedDiagnostic = new ExpectedDiagnostic( + DuplicateFluentApiMethod.Descriptor, + "WithName", + (10, 6), (11, 6)); + RunGeneratorAndCheckDiagnostics("DuplicateMethodPartialClass", "Student1|Student2", expectedDiagnostic); } [Fact] @@ -77,7 +88,7 @@ public void CanDetectReservedMethod1() "InitialStep", (17, 6)); - RunGeneratorAndCheckDiagnostics("ReservedMethodClass1", expectedDiagnostic1, expectedDiagnostic2); + RunGeneratorAndCheckDiagnostics("ReservedMethodClass1", "Student", expectedDiagnostic1, expectedDiagnostic2); } [Fact] @@ -88,14 +99,18 @@ public void CanDetectReservedMethod2() "InitialStep", (12, 6)); - RunGeneratorAndCheckDiagnostics("ReservedMethodClass2", expectedDiagnostic1); + RunGeneratorAndCheckDiagnostics("ReservedMethodClass2", "Student", expectedDiagnostic1); } - private void RunGeneratorAndCheckDiagnostics(string testClassFolder, + private void RunGeneratorAndCheckDiagnostics( + string testClassFolder, + string classes, params ExpectedDiagnostic[] expectedDiagnostics) { - (string source, _) = ReadSource(testClassFolder, "Student"); - Diagnostic[] diagnostics = ManualGenerator.RunGeneratorsAndGetDiagnostics(source).ToArray(); + string[] splitClasses = classes.Split('|'); + SourceWithFix[] sourceCodeWithFixes = splitClasses.Select(c => ReadSource(testClassFolder, c)).ToArray(); + string[] sourceCode = sourceCodeWithFixes.Select(c => c.Source).ToArray(); + Diagnostic[] diagnostics = ManualGenerator.RunGeneratorsAndGetDiagnostics(sourceCode).ToArray(); Assert.Equal(expectedDiagnostics.Length, diagnostics.Length); diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/AnalyzerAndCodeFixVerifier.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/AnalyzerAndCodeFixVerifier.cs index bb61db0..cc8c7d9 100644 --- a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/AnalyzerAndCodeFixVerifier.cs +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/AnalyzerAndCodeFixVerifier.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -23,26 +24,34 @@ internal static DiagnosticResult Diagnostic(string diagnosticId) } internal static async Task VerifyCodeFixAsync( - string source, - string? fixedSource, + SourceWithFix source, params DiagnosticResult[] expected) { - var test = new CodeFixTest(source, fixedSource, expected); + await VerifyCodeFixAsync(new SourceWithFix[] { source }, expected); + } + + internal static async Task VerifyCodeFixAsync( + IReadOnlyCollection sourceCode, + params DiagnosticResult[] expected) + { + CodeFixTest test = new CodeFixTest(sourceCode, expected); await test.RunAsync(CancellationToken.None); } private class CodeFixTest : CSharpCodeFixTest { internal CodeFixTest( - string source, - string? fixedSource, + IReadOnlyCollection sourceCode, params DiagnosticResult[] expected) { - TestCode = source; - - if (fixedSource != null) + foreach (SourceWithFix source in sourceCode) { - FixedCode = fixedSource; + TestState.Sources.Add(source.Source); + + if (source.FixedSource != null) + { + FixedState.Sources.Add(source.FixedSource); + } } ExpectedDiagnostics.AddRange(expected); diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/SourceWithFix.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/SourceWithFix.cs new file mode 100644 index 0000000..6b46f95 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/SourceWithFix.cs @@ -0,0 +1,3 @@ +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.Helpers; + +internal record SourceWithFix(string Source, string? FixedSource); \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/TestSourceCodeReader.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/TestSourceCodeReader.cs index f569c7b..f0703e0 100644 --- a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/TestSourceCodeReader.cs +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/Helpers/TestSourceCodeReader.cs @@ -4,11 +4,11 @@ namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.Helpers; internal static class TestSourceCodeReader { - internal static (string source, string fixedSource) ReadSource(string testClassFolder, string @class) + internal static SourceWithFix ReadSource(string testClassFolder, string @class) { string source = ReadTestClassCode(testClassFolder, $"{@class}.cs"); string fixedSource = TryReadTestClassCode(testClassFolder, $"{@class}.fixed.txt") ?? source; - return (source, fixedSource); + return new SourceWithFix(source, fixedSource); } private static string ReadTestClassCode(string testClassFolder, string file) diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/DuplicateMethodPartialClass/Student1.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/DuplicateMethodPartialClass/Student1.cs new file mode 100644 index 0000000..9cdbc79 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/DuplicateMethodPartialClass/Student1.cs @@ -0,0 +1,13 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.DuplicateMethodPartialClass; + +[FluentApi] +public partial class Student +{ + [FluentMember(0, "WithName")] + public string FirstName { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/DuplicateMethodPartialClass/Student2.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/DuplicateMethodPartialClass/Student2.cs new file mode 100644 index 0000000..72779d5 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/DuplicateMethodPartialClass/Student2.cs @@ -0,0 +1,12 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.DuplicateMethodPartialClass; + +public partial class Student +{ + [FluentMember(1, "WithName")] + public string LastName { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecord/Student.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecord/Student.cs new file mode 100644 index 0000000..2a445ed --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecord/Student.cs @@ -0,0 +1,12 @@ +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.GetMissingSetRecord; + +[FluentApi] +public record Student +{ + [FluentMember(0)] + int Semester { get; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecord/Student.fixed.txt b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecord/Student.fixed.txt new file mode 100644 index 0000000..05955d0 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecord/Student.fixed.txt @@ -0,0 +1,12 @@ +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.GetMissingSetRecord; + +[FluentApi] +public record Student +{ + [FluentMember(0)] + int Semester { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecordStruct/Student.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecordStruct/Student.cs new file mode 100644 index 0000000..4ecadf3 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecordStruct/Student.cs @@ -0,0 +1,12 @@ +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.GetMissingSetRecordStruct; + +[FluentApi] +public record struct Student +{ + [FluentMember(0)] + int Semester { get; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecordStruct/Student.fixed.txt b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecordStruct/Student.fixed.txt new file mode 100644 index 0000000..d2bcce3 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetRecordStruct/Student.fixed.txt @@ -0,0 +1,12 @@ +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.GetMissingSetRecordStruct; + +[FluentApi] +public record struct Student +{ + [FluentMember(0)] + int Semester { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetStruct/Student.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetStruct/Student.cs new file mode 100644 index 0000000..7a287b4 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetStruct/Student.cs @@ -0,0 +1,12 @@ +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.GetMissingSetStruct; + +[FluentApi] +public struct Student +{ + [FluentMember(0)] + int Semester { get; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetStruct/Student.fixed.txt b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetStruct/Student.fixed.txt new file mode 100644 index 0000000..7309a07 --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/GetMissingSetStruct/Student.fixed.txt @@ -0,0 +1,12 @@ +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.GetMissingSetStruct; + +[FluentApi] +public struct Student +{ + [FluentMember(0)] + int Semester { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student1.cs similarity index 62% rename from src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student.cs rename to src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student1.cs index 8ead6aa..34824e3 100644 --- a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student.cs +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student1.cs @@ -1,3 +1,5 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 // ReSharper disable All using M31.FluentApi.Attributes; @@ -8,5 +10,5 @@ namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.PartialClass; public partial class Student { [FluentMember(0)] - public int Semester { get; private set; } + public string FirstName { get; private set; } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student2.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student2.cs new file mode 100644 index 0000000..91ea99a --- /dev/null +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/PartialClass/Student2.cs @@ -0,0 +1,13 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.PartialClass; + +public partial class Student +{ + [FluentMember(1)] + public string LastName { get; private set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs b/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs index 74bc57f..50d7b74 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs @@ -253,6 +253,18 @@ public void CanExecuteGenericOverloadedPrivateMethodClass() Assert.Equal(new string[] { "Called Method1(in T, ref string)" }, student7.Logs); } + [Fact, Priority(1)] + public void CanExecutePartialClass() + { + var student = TestClasses.Abstract.PartialClass + .CreateStudent + .WithFirstName("Alice") + .WithLastName("King"); + + Assert.Equal("Alice", student.FirstName); + Assert.Equal("King", student.LastName); + } + [Fact, Priority(1)] public void CanExecutePrivateConstructorClass() { diff --git a/src/M31.FluentApi.Tests/CodeGeneration/Helpers/ListAndDictionary.cs b/src/M31.FluentApi.Tests/CodeGeneration/Helpers/ListAndDictionary.cs index 37463fd..8835f1a 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/Helpers/ListAndDictionary.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/Helpers/ListAndDictionary.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace M31.FluentApi.Tests.CodeGeneration.Helpers; @@ -11,50 +12,50 @@ public class ListAndDictionary : List, IDictionary> GetEnumerator() { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public void Add(KeyValuePair item) { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public bool Contains(KeyValuePair item) { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public void CopyTo(KeyValuePair[] array, int arrayIndex) { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public bool Remove(KeyValuePair item) { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public bool IsReadOnly { get; } = false; public void Add(TKey key, TValue value) { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public bool ContainsKey(TKey key) { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public bool TryGetValue(TKey key, out TValue value) { - throw new System.NotImplementedException(); + throw new NotSupportedException(); } public TValue this[TKey key] { - get => throw new System.NotImplementedException(); - set => throw new System.NotImplementedException(); + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); } public ICollection Keys { get; } = new List(); diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClass/Address.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClass/Address.cs new file mode 100644 index 0000000..9596251 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClass/Address.cs @@ -0,0 +1,20 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable all + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.FluentLambdaClass; + +[FluentApi] +public class Address +{ + [FluentMember(0)] + public string HouseNumber { get; set; } + + [FluentMember(1)] + public string Street { get; set; } + + [FluentMember(2, "InCity")] + public string City { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClass/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClass/Student.cs index 5eae71b..05c4798 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClass/Student.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClass/Student.cs @@ -1,5 +1,6 @@ // Non-nullable member is uninitialized #pragma warning disable CS8618 +// ReSharper disable all using M31.FluentApi.Attributes; @@ -13,17 +14,4 @@ public class Student [FluentLambda(1)] public Address Address { get; set; } -} - -[FluentApi] -public class Address -{ - [FluentMember(0)] - public string HouseNumber { get; set; } - - [FluentMember(1)] - public string Street { get; set; } - - [FluentMember(2, "InCity")] - public string City { get; set; } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/Address.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/Address.cs new file mode 100644 index 0000000..d2f28ec --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/Address.cs @@ -0,0 +1,20 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable all + +using M31.FluentApi.Attributes; + +namespace SomeOtherNamespace; + +[FluentApi] +public class Address +{ + [FluentMember(0)] + public string HouseNumber { get; set; } + + [FluentMember(1)] + public string Street { get; set; } + + [FluentMember(2, "InCity")] + public string City { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/CreateAddress.g.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/CreateAddress.g.cs index 870b920..fbf7d47 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/CreateAddress.g.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/CreateAddress.g.cs @@ -6,7 +6,6 @@ #nullable enable using M31.FluentApi.Attributes; -using SomeOtherNamespace; namespace SomeOtherNamespace; diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/Student.cs index 662c79c..c9f93ab 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/Student.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaClassInDifferentNamespace/Student.cs @@ -1,35 +1,18 @@ // Non-nullable member is uninitialized - #pragma warning disable CS8618 +// ReSharper disable all using M31.FluentApi.Attributes; using SomeOtherNamespace; -namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.FluentLambdaClassInDifferentNamespace -{ - [FluentApi] - public class Student - { - [FluentMember(0)] - public string Name { get; set; } - - [FluentLambda(1)] - public Address Address { get; set; } - } -} +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.FluentLambdaClassInDifferentNamespace; -namespace SomeOtherNamespace +[FluentApi] +public class Student { - [FluentApi] - public class Address - { - [FluentMember(0)] - public string HouseNumber { get; set; } - - [FluentMember(1)] - public string Street { get; set; } + [FluentMember(0)] + public string Name { get; set; } - [FluentMember(2, "InCity")] - public string City { get; set; } - } + [FluentLambda(1)] + public Address Address { get; set; } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaNullablePropertyClass/Address.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaNullablePropertyClass/Address.cs new file mode 100644 index 0000000..d82b623 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaNullablePropertyClass/Address.cs @@ -0,0 +1,20 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable all + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.FluentLambdaNullablePropertyClass; + +[FluentApi] +public class Address +{ + [FluentMember(0)] + public string HouseNumber { get; set; } + + [FluentMember(1)] + public string Street { get; set; } + + [FluentMember(2, "InCity")] + public string City { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaNullablePropertyClass/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaNullablePropertyClass/Student.cs index 3c18630..27ffe12 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaNullablePropertyClass/Student.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaNullablePropertyClass/Student.cs @@ -1,5 +1,6 @@ // Non-nullable member is uninitialized #pragma warning disable CS8618 +// ReSharper disable all using M31.FluentApi.Attributes; @@ -14,17 +15,4 @@ public class Student [FluentLambda(1)] [FluentNullable] public Address? Address { get; set; } -} - -[FluentApi] -public class Address -{ - [FluentMember(0)] - public string HouseNumber { get; set; } - - [FluentMember(1)] - public string Street { get; set; } - - [FluentMember(2, "InCity")] - public string City { get; set; } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaRecursiveClass/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaRecursiveClass/Student.cs index 00a8199..0769793 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaRecursiveClass/Student.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaRecursiveClass/Student.cs @@ -1,5 +1,6 @@ // Non-nullable member is uninitialized #pragma warning disable CS8618 +// ReSharper disable all using M31.FluentApi.Attributes; diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaSingleStepClass/Address.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaSingleStepClass/Address.cs new file mode 100644 index 0000000..8221382 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaSingleStepClass/Address.cs @@ -0,0 +1,20 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable all + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.FluentLambdaSingleStepClass; + +[FluentApi] +public class Address +{ + [FluentMember(0)] + public string HouseNumber { get; set; } + + [FluentMember(1)] + public string Street { get; set; } + + [FluentMember(2, "InCity")] + public string City { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaSingleStepClass/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaSingleStepClass/Student.cs index cef0731..9b362cb 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaSingleStepClass/Student.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/FluentLambdaSingleStepClass/Student.cs @@ -1,5 +1,6 @@ // Non-nullable member is uninitialized #pragma warning disable CS8618 +// ReSharper disable all using M31.FluentApi.Attributes; @@ -10,17 +11,4 @@ public class Student { [FluentLambda(0)] public Address Address { get; set; } -} - -[FluentApi] -public class Address -{ - [FluentMember(0)] - public string HouseNumber { get; set; } - - [FluentMember(1)] - public string Street { get; set; } - - [FluentMember(2, "InCity")] - public string City { get; set; } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/CreateStudent.expected.txt b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/CreateStudent.expected.txt new file mode 100644 index 0000000..e7fc837 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/CreateStudent.expected.txt @@ -0,0 +1,70 @@ +// +// This code was generated by the library M31.FluentAPI. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#nullable enable + +using M31.FluentApi.Attributes; +using System.Reflection; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.PartialClass; + +public class CreateStudent : + CreateStudent.ICreateStudent, + CreateStudent.IWithFirstName, + CreateStudent.IWithLastName +{ + private readonly Student student; + private static readonly PropertyInfo firstNamePropertyInfo; + private static readonly PropertyInfo lastNamePropertyInfo; + + static CreateStudent() + { + firstNamePropertyInfo = typeof(Student).GetProperty("FirstName", BindingFlags.Instance | BindingFlags.Public)!; + lastNamePropertyInfo = typeof(Student).GetProperty("LastName", BindingFlags.Instance | BindingFlags.Public)!; + } + + private CreateStudent() + { + student = new Student(); + } + + public static ICreateStudent InitialStep() + { + return new CreateStudent(); + } + + public static IWithLastName WithFirstName(string firstName) + { + CreateStudent createStudent = new CreateStudent(); + CreateStudent.firstNamePropertyInfo.SetValue(createStudent.student, firstName); + return createStudent; + } + + IWithLastName IWithFirstName.WithFirstName(string firstName) + { + CreateStudent.firstNamePropertyInfo.SetValue(student, firstName); + return this; + } + + Student IWithLastName.WithLastName(string lastName) + { + CreateStudent.lastNamePropertyInfo.SetValue(student, lastName); + return student; + } + + public interface ICreateStudent : IWithFirstName + { + } + + public interface IWithFirstName + { + IWithLastName WithFirstName(string firstName); + } + + public interface IWithLastName + { + Student WithLastName(string lastName); + } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/CreateStudent.g.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/CreateStudent.g.cs new file mode 100644 index 0000000..e7fc837 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/CreateStudent.g.cs @@ -0,0 +1,70 @@ +// +// This code was generated by the library M31.FluentAPI. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#nullable enable + +using M31.FluentApi.Attributes; +using System.Reflection; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.PartialClass; + +public class CreateStudent : + CreateStudent.ICreateStudent, + CreateStudent.IWithFirstName, + CreateStudent.IWithLastName +{ + private readonly Student student; + private static readonly PropertyInfo firstNamePropertyInfo; + private static readonly PropertyInfo lastNamePropertyInfo; + + static CreateStudent() + { + firstNamePropertyInfo = typeof(Student).GetProperty("FirstName", BindingFlags.Instance | BindingFlags.Public)!; + lastNamePropertyInfo = typeof(Student).GetProperty("LastName", BindingFlags.Instance | BindingFlags.Public)!; + } + + private CreateStudent() + { + student = new Student(); + } + + public static ICreateStudent InitialStep() + { + return new CreateStudent(); + } + + public static IWithLastName WithFirstName(string firstName) + { + CreateStudent createStudent = new CreateStudent(); + CreateStudent.firstNamePropertyInfo.SetValue(createStudent.student, firstName); + return createStudent; + } + + IWithLastName IWithFirstName.WithFirstName(string firstName) + { + CreateStudent.firstNamePropertyInfo.SetValue(student, firstName); + return this; + } + + Student IWithLastName.WithLastName(string lastName) + { + CreateStudent.lastNamePropertyInfo.SetValue(student, lastName); + return student; + } + + public interface ICreateStudent : IWithFirstName + { + } + + public interface IWithFirstName + { + IWithLastName WithFirstName(string firstName); + } + + public interface IWithLastName + { + Student WithLastName(string lastName); + } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/Student1.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/Student1.cs new file mode 100644 index 0000000..76f664d --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/Student1.cs @@ -0,0 +1,14 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.PartialClass; + +[FluentApi] +public partial class Student +{ + [FluentMember(0)] + public string FirstName { get; private set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/Student2.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/Student2.cs new file mode 100644 index 0000000..5e81e70 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/PartialClass/Student2.cs @@ -0,0 +1,13 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.PartialClass; + +public partial class Student +{ + [FluentMember(1)] + public string LastName { get; private set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs index 0d81de5..1025cb0 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs @@ -21,11 +21,11 @@ internal class TestDataProvider : IEnumerable new object[] { "Abstract", "DefaultFluentMethodNameClass", "Student" }, new object[] { "Abstract", "EmptyClass", "Student" }, new object[] { "Abstract", "FluentDefaultMemberClass", "Student" }, - new object[] { "Abstract", "FluentLambdaClass", "Student" }, - new object[] { "Abstract", "FluentLambdaClassInDifferentNamespace", "Student" }, - new object[] { "Abstract", "FluentLambdaNullablePropertyClass", "Student" }, + new object[] { "Abstract", "FluentLambdaClass", "Student|Address" }, + new object[] { "Abstract", "FluentLambdaClassInDifferentNamespace", "Student|Address" }, + new object[] { "Abstract", "FluentLambdaNullablePropertyClass", "Student|Address" }, new object[] { "Abstract", "FluentLambdaRecursiveClass", "Student" }, - new object[] { "Abstract", "FluentLambdaSingleStepClass", "Student" }, + new object[] { "Abstract", "FluentLambdaSingleStepClass", "Student|Address" }, new object[] { "Abstract", "FluentMethodClass", "Student" }, new object[] { "Abstract", "FluentMethodDefaultValuesClass", "Student" }, new object[] { "Abstract", "FluentMethodParameterModifiersClass", "Student" }, @@ -55,6 +55,7 @@ internal class TestDataProvider : IEnumerable new object[] { "Abstract", "NullablePredicateAndCollectionClass", "Student" }, new object[] { "Abstract", "OneMemberClass", "Student" }, new object[] { "Abstract", "OverloadedMethodClass", "Student" }, + new object[] { "Abstract", "PartialClass", "Student1|Student2" }, new object[] { "Abstract", "PredicateClass", "Student" }, new object[] { "Abstract", "PredicatePrivateFieldClass", "Student" }, new object[] { "Abstract", "PrivateConstructorClass", "Student" }, diff --git a/src/M31.FluentApi.Tests/Helpers/ManualGenerator.cs b/src/M31.FluentApi.Tests/Helpers/ManualGenerator.cs index 347ebed..23ca3da 100644 --- a/src/M31.FluentApi.Tests/Helpers/ManualGenerator.cs +++ b/src/M31.FluentApi.Tests/Helpers/ManualGenerator.cs @@ -12,23 +12,23 @@ namespace M31.FluentApi.Tests.Helpers; internal static class ManualGenerator { - internal static CSharpCompilation GetCompilation(string sourceCode) + internal static CSharpCompilation GetCompilation(IReadOnlyCollection sourceCode) { - SyntaxTree inputSyntaxTree = CSharpSyntaxTree.ParseText(sourceCode); + SyntaxTree[] inputSyntaxTrees = sourceCode.Select(c => CSharpSyntaxTree.ParseText(c)).ToArray(); IEnumerable references = AppDomain.CurrentDomain.GetAssemblies() .Where(assembly => !assembly.IsDynamic) .Select(assembly => MetadataReference .CreateFromFile(assembly.Location)); CSharpCompilation compilation = CSharpCompilation.Create("SourceGeneratorTests", - new[] { inputSyntaxTree }, + inputSyntaxTrees, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); return compilation; } - internal static ImmutableArray RunGeneratorsAndGetDiagnostics(string sourceCode) + internal static ImmutableArray RunGeneratorsAndGetDiagnostics(IReadOnlyCollection sourceCode) { CSharpCompilation compilation = GetCompilation(sourceCode); SourceGenerator generator = new SourceGenerator(); @@ -41,7 +41,7 @@ internal static ImmutableArray RunGeneratorsAndGetDiagnostics(string return diagnostics; } - internal static GeneratorOutputs RunGenerators(string sourceCode) + internal static GeneratorOutputs RunGenerators(IReadOnlyCollection sourceCode) { CSharpCompilation compilation = GetCompilation(sourceCode); SourceGenerator generator = new SourceGenerator(); @@ -54,8 +54,11 @@ internal static GeneratorOutputs RunGenerators(string sourceCode) Assert.Equal(0, diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error)); GeneratorOutput[] generatorOutputs = - outputCompilation.SyntaxTrees.Skip(1).Select(GetGeneratorOutput).OfType().ToArray(); - // First syntax tree is the input syntax tree, the second syntax tree is the first output of interest. For some + outputCompilation.SyntaxTrees + .Skip(sourceCode.Count) + .Select(GetGeneratorOutput) + .OfType().ToArray(); + // First syntax trees are the input trees, the next syntax trees are the outputs of interest. For some // tests more than one generator output is created, e.g. for the FluentLambdaClass test. return new GeneratorOutputs(generatorOutputs); diff --git a/src/M31.FluentApi.Tests/Helpers/SyntaxExtensions.cs b/src/M31.FluentApi.Tests/Helpers/SyntaxExtensions.cs index ad9018c..ecbfbb6 100644 --- a/src/M31.FluentApi.Tests/Helpers/SyntaxExtensions.cs +++ b/src/M31.FluentApi.Tests/Helpers/SyntaxExtensions.cs @@ -10,7 +10,7 @@ internal static class SyntaxExtensions internal static TypeDeclarationSyntax? GetFluentApiTypeDeclaration(this SyntaxTree syntaxTree) { SyntaxNode root = syntaxTree.GetRoot(); - TypeDeclarationSyntax? typeDeclaration = (TypeDeclarationSyntax?)root.Find(n => n.IsTypeDeclarationOfInterest()); + TypeDeclarationSyntax? typeDeclaration = (TypeDeclarationSyntax?)root.Find(n => n.IsClassStructOrRecordSyntax()); return typeDeclaration; } diff --git a/src/M31.FluentApi.Tests/Helpers/TestClassCodeGenerator.cs b/src/M31.FluentApi.Tests/Helpers/TestClassCodeGenerator.cs index bd66705..ecc6f4b 100644 --- a/src/M31.FluentApi.Tests/Helpers/TestClassCodeGenerator.cs +++ b/src/M31.FluentApi.Tests/Helpers/TestClassCodeGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -11,39 +12,43 @@ namespace M31.FluentApi.Tests.Helpers; internal class TestClassCodeGenerator { - private TestClassCodeGenerator(string classPath, string className) + private TestClassCodeGenerator(string classPath, IReadOnlyCollection classNames) { ClassPath = classPath; - ClassName = className; + ClassNames = classNames; } internal string ClassPath { get; } - internal string ClassName { get; } + internal IReadOnlyCollection ClassNames { get; } internal static TestClassCodeGenerator Create(params string[] testClassPathAndName) { string classPath = Path.Join(testClassPathAndName[0..^1]); - string className = testClassPathAndName[^1]; - return new TestClassCodeGenerator(classPath, className); + string classes = testClassPathAndName[^1]; + string[] splitClasses = classes.Split('|'); + return new TestClassCodeGenerator(classPath, splitClasses); } internal GeneratorOutputs RunGenerators() { - string code = File.ReadAllText(PathToTestDataFile(ClassPath, $"{ClassName}.cs")); - return ManualGenerator.RunGenerators(code); + return ManualGenerator.RunGenerators(ReadSourceCode()); } internal (SemanticModel semanticModel, TypeDeclarationSyntax? typeDeclaration) GetSemanticModelAndTypeDeclaration() { - string code = File.ReadAllText(PathToTestDataFile(ClassPath, $"{ClassName}.cs")); - CSharpCompilation compilation = ManualGenerator.GetCompilation(code); + CSharpCompilation compilation = ManualGenerator.GetCompilation(ReadSourceCode()); SyntaxTree syntaxTree = compilation.SyntaxTrees.First(); SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree); TypeDeclarationSyntax? typeDeclaration = syntaxTree.GetFluentApiTypeDeclaration(); return (semanticModel, typeDeclaration); } + private string[] ReadSourceCode() + { + return ClassNames.Select(n => File.ReadAllText(PathToTestDataFile(ClassPath, $"{n}.cs"))).ToArray(); + } + internal ClassInfoResult CreateFluentApiClassInfoResult() { (SemanticModel semanticModel, TypeDeclarationSyntax? typeDeclaration) = GetSemanticModelAndTypeDeclaration(); diff --git a/src/M31.FluentApi.Tests/M31.FluentApi.Tests.csproj b/src/M31.FluentApi.Tests/M31.FluentApi.Tests.csproj index 1ab522f..32f36a6 100644 --- a/src/M31.FluentApi.Tests/M31.FluentApi.Tests.csproj +++ b/src/M31.FluentApi.Tests/M31.FluentApi.Tests.csproj @@ -30,9 +30,4 @@ - - - - - diff --git a/src/M31.FluentApi/M31.FluentApi.csproj b/src/M31.FluentApi/M31.FluentApi.csproj index 649ac82..d1f858f 100644 --- a/src/M31.FluentApi/M31.FluentApi.csproj +++ b/src/M31.FluentApi/M31.FluentApi.csproj @@ -7,7 +7,7 @@ enable true true - 1.4.0 + 1.5.0 Kevin Schaal Generate fluent builders in C#. fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration