diff --git a/.github/workflows/build-nuget.yml b/.github/workflows/build-nuget.yml new file mode 100644 index 0000000..fff42b3 --- /dev/null +++ b/.github/workflows/build-nuget.yml @@ -0,0 +1,104 @@ +name: Build NuGet package + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.400 + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'global.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + run: dotnet restore AlephMapper.slnx + + - name: Build solution + run: dotnet build AlephMapper.slnx --configuration Release --no-restore + + - name: Run tests + run: dotnet test AlephMapper.slnx --configuration Release --no-build --no-restore -- --report-trx --results-directory artifacts/test-results + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: artifacts/test-results + if-no-files-found: warn + + - name: Pack NuGet package + run: dotnet pack source/AlephMapper.csproj --configuration Release --no-restore -p:BuildNumber=${{ github.run_number }} --output artifacts/nuget + + - name: Verify package consumption + shell: bash + run: | + package_file=$(find artifacts/nuget -name 'AlephMapper.*.nupkg' -print -quit) + package_version=$(basename "$package_file" | sed -E 's/^AlephMapper\.(.*)\.nupkg$/\1/') + mkdir -p artifacts/package-smoke + + cat > artifacts/package-smoke/PackageSmoke.csproj < + + net10.0 + enable + enable + + + + + + EOF + + cat > artifacts/package-smoke/Mapper.cs <<'EOF' + using AlephMapper; + + namespace PackageSmoke; + + public sealed class Source + { + public string Name { get; set; } = string.Empty; + } + + public sealed class Destination + { + public string Name { get; set; } = string.Empty; + } + + public static partial class Mapper + { + [Projectable] + public static Destination Map(Source source) => new() { Name = source.Name }; + } + EOF + + dotnet restore artifacts/package-smoke/PackageSmoke.csproj --source artifacts/nuget + dotnet build artifacts/package-smoke/PackageSmoke.csproj --configuration Release --no-restore + + - name: Upload NuGet package + uses: actions/upload-artifact@v4 + with: + name: AlephMapper-nuget + path: artifacts/nuget/*.nupkg + if-no-files-found: error diff --git a/README.md b/README.md index 12b4351..6ac41b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua) +[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua) ## Terms of use[?](https://github.com/Tyrrrz/.github/blob/master/docs/why-so-political.md) @@ -72,7 +72,7 @@ dotnet add package AlephMapper Using `PackageReference`: ```xml - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -82,7 +82,7 @@ With Central Package Management: ```xml - + @@ -101,18 +101,36 @@ When referencing the generator directly from source: `PrivateAssets="all"` prevents AlephMapper from flowing transitively to consumers of your library. `IncludeAssets` makes its analyzer and source-generator assets available during compilation. +### Compiler compatibility + +AlephMapper 0.7.0 requires a Roslyn compiler host compatible with `Microsoft.CodeAnalysis` 4.14 or later. This version uses Roslyn's embedded-marker support so its generated configuration attributes remain private to the consuming assembly, including when `InternalsVisibleTo` is used. + +### Migration: `[Expressive]` to `[Projectable]` + +`[Expressive]` has been renamed to `[Projectable]`. Replace every attribute use directly; no compatibility attribute is emitted. + +```csharp +// Before +[Expressive] +public static PersonDto Map(Person person) => new(); + +// After +[Projectable] +public static PersonDto Map(Person person) => new(); +``` + ## Quick start Mapping methods must be `static`, expression-bodied, and declared in a `static partial` class. -Add `using AlephMapper;`, then apply `[Expressive]` to a mapping method or its containing class: +Add `using AlephMapper;`, then apply `[Projectable]` to a mapping method or its containing class: ```csharp using AlephMapper; public static partial class PersonMapper { - [Expressive] + [Projectable] public static PersonDto MapPerson(Employee employee) => new() { Id = employee.EmployeeId, @@ -163,7 +181,7 @@ Supported expression-bodied methods can call other mapping or helper methods. Al ```csharp public static partial class OrderMapper { - [Expressive] + [Projectable] public static OrderDto MapOrder(Order order) => new() { Id = order.Id, @@ -185,12 +203,12 @@ public static partial class OrderMapper } ``` -`[Expressive]` is not limited to object projections. A method returning `bool` generates an `Expression>`, allowing statically known conditions to be composed as ordinary methods: +`[Projectable]` is not limited to object projections. A method returning `bool` generates an `Expression>`, allowing statically known conditions to be composed as ordinary methods: ```csharp public static partial class EmployeeConditions { - [Expressive] + [Projectable] public static bool IsEligible(Employee employee) => IsActive(employee) && HasRequiredExperience(employee, 3); @@ -220,7 +238,7 @@ A mapping may accept values after its source parameter. AlephMapper moves those ```csharp public static partial class EmployeeMapper { - [Expressive] + [Projectable] public static EmployeeDto Map( Employee employee, int currentYear) => new() @@ -266,7 +284,7 @@ public static class ProductExtensions public static partial class ProductMapper { - [Expressive] + [Projectable] public static ProductDto Map(Product product) => new() { Name = product.Name, @@ -298,7 +316,7 @@ public static partial class AddressMapper }; } -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class PersonMapper { public static PersonDto ToDto(Person person) => new() @@ -347,7 +365,7 @@ Modern C# extension blocks are not currently supported. C# null-conditional access (`?.`) is not directly supported in expression trees. Configure its treatment with `NullConditionalRewrite`: ```csharp -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class PersonMapper { public static PersonDto Map(Person person) => new() @@ -470,11 +488,11 @@ Adaptation is explicit: AlephMapper does not scan for compatible types. Invalid | Attribute | Generated member | | --- | --- | -| `[Expressive]` | `Expression(...)` returning `Expression>` | +| `[Projectable]` | `Expression(...)` returning `Expression>` | | `[Updatable]` | An overload with a final destination parameter named `target` | | `[Adapt]` | The requested adapted map, expression, and/or update members | -Attributes can be applied to individual methods. `[Expressive]` and `[Updatable]` can also be applied to the containing class. +Attributes can be applied to individual methods. `[Projectable]` and `[Updatable]` can also be applied to the containing class. AlephMapper is best suited to object initializers, predicates, constructor calls, member access, conversions, LINQ operations, and small expression-bodied methods that it can inline. Not every valid C# construct can be represented in an expression tree, and not every expression-tree operation can be translated by every query provider. diff --git a/docs/Adapt-Attribute.md b/docs/Adapt-Attribute.md index 45540a4..7b1e204 100644 --- a/docs/Adapt-Attribute.md +++ b/docs/Adapt-Attribute.md @@ -25,7 +25,7 @@ Use `[Adapt]` when a mapping body should be shared by explicitly named type pair For example, `Person` and `Employee` can both contain the fields used by a template, while `PersonDto` and `EmployeeDto` can both receive the initializer assignments. A single `Person -> PersonDto` template can therefore produce a separate `Employee -> EmployeeDto` API. -Use `[Expressive]` when the generated expression is for the original method's declared signature. Use `[Adapt]` when the generated API should use a different, explicit source and/or destination type. The two attributes can be applied to the same template method. +Use `[Projectable]` when the generated expression is for the original method's declared signature. Use `[Adapt]` when the generated API should use a different, explicit source and/or destination type. The two attributes can be applied to the same template method. ## Public API @@ -55,7 +55,7 @@ public sealed class AdaptAttribute : Attribute `SourceType` and `DestinationType` are required `typeof(...)` constructor arguments. Each `[Adapt]` is independent, so a method may be adapted to multiple pairs. -`NullConditionalRewrite` applies while helper methods are inlined for that adaptation. It uses the same policies as `[Expressive]`: +`NullConditionalRewrite` applies while helper methods are inlined for that adaptation. It uses the same policies as `[Projectable]`: | Value | Effect | | --- | --- | @@ -217,7 +217,7 @@ The feature is implemented as part of the incremental source generator. The foll | [`source/Generation/MapperFileEmitter.cs`](../source/Generation/MapperFileEmitter.cs) | Recreates the namespace and containing partial-type hierarchy and renders the generated file. | | [`source/Diagnostics/DiagnosticDescriptors.cs`](../source/Diagnostics/DiagnosticDescriptors.cs) | Defines `AM0005`–`AM0015`. | -The older `[Expressive]` and `[Updatable]` behaviors are emitted by their own focused emitters. All three features share the mapping model, helper inliner, generated-file context, and output renderer. +`[Projectable]` and `[Updatable]` are emitted by their own focused emitters. All three features share the mapping model, helper inliner, generated-file context, and output renderer. ## Generation pipeline @@ -226,8 +226,8 @@ The incremental pipeline is registered in [`source/AlephSourceGenerator.cs`](../ 1. `AttributeSourceEmitter` adds `AlephMapper.Attributes.g.cs` after initialization so the consumer can use the attributes. 2. `MappingMethodCandidate` identifies method declarations contained in classes. 3. `MappingModelFactory` filters to static classes, resolves symbols, requires at least one parameter and an expression body, and collects the method's adaptation attributes. -4. `MapperSourceOutput` groups mapping models by containing mapper type. A mapper produces output when it is partial and contains an expressive, updatable, or adapted mapping. -5. For each eligible mapping, the output dispatcher runs `ExpressiveMemberEmitter`, `AdaptationMemberEmitter`, and `UpdatableMemberEmitter`. +4. `MapperSourceOutput` groups mapping models by containing mapper type. A mapper produces output when it is partial and contains a projectable, updatable, or adapted mapping. +5. For each eligible mapping, the output dispatcher runs `ProjectableMemberEmitter`, `AdaptationMemberEmitter`, and `UpdatableMemberEmitter`. 6. The adaptation emitter processes every `AdaptationModel` independently: 1. Decodes the requested flags and verifies the naming requirement. 2. Rejects duplicate source/destination pairs on the same template. diff --git a/examples/SampleApp/Mappers/AdaptExampleMapper.cs b/examples/SampleApp/Mappers/AdaptExampleMapper.cs index bd0faf3..657c3df 100644 --- a/examples/SampleApp/Mappers/AdaptExampleMapper.cs +++ b/examples/SampleApp/Mappers/AdaptExampleMapper.cs @@ -6,7 +6,7 @@ namespace SampleApp.Mappers; /// /// Demonstrates [Adapt]: one mapping template reused for an explicitly declared -/// different source/destination pair. Unlike [Expressive], [Adapt] is not for the +/// different source/destination pair. Unlike [Projectable], [Adapt] is not for the /// exact method signature types; it structurally substitutes the template source /// and destination with the explicit types from the attribute. /// diff --git a/examples/SampleApp/Mappers/AddressMapper.cs b/examples/SampleApp/Mappers/AddressMapper.cs index 4342be8..4552d6d 100644 --- a/examples/SampleApp/Mappers/AddressMapper.cs +++ b/examples/SampleApp/Mappers/AddressMapper.cs @@ -1,4 +1,4 @@ -using AlephMapper; +using AlephMapper; using SampleApp.Entities; using SampleApp.Models; @@ -7,7 +7,7 @@ namespace SampleApp.Mappers; public static partial class AddressMapper { // Entity to DTO mapping with expression-bodied syntax - [Expressive] + [Projectable] public static AddressDto ToDto(this Address entity) => new() { Street = entity.StreetAddress, diff --git a/examples/SampleApp/Mappers/EmployeeMapper.cs b/examples/SampleApp/Mappers/EmployeeMapper.cs index 8d02942..7f36107 100644 --- a/examples/SampleApp/Mappers/EmployeeMapper.cs +++ b/examples/SampleApp/Mappers/EmployeeMapper.cs @@ -11,7 +11,7 @@ namespace SampleApp.Mappers; /// public static partial class EmployeeMapper { - [Expressive] + [Projectable] [Updatable] public static EmployeeSummaryDto ToSummary(Employee emp, int year) => new() { diff --git a/examples/SampleApp/Mappers/PersonMapper.cs b/examples/SampleApp/Mappers/PersonMapper.cs index aba4030..e0d44b9 100644 --- a/examples/SampleApp/Mappers/PersonMapper.cs +++ b/examples/SampleApp/Mappers/PersonMapper.cs @@ -1,4 +1,4 @@ -using AlephMapper; +using AlephMapper; using SampleApp.Entities; using SampleApp.Models; @@ -7,9 +7,9 @@ namespace SampleApp.Mappers; public static partial class PersonMapper { // Main entity to DTO mapping with expression-bodied syntax - [Expressive] + [Projectable] [Updatable(CollectionProperties = CollectionPropertiesPolicy.Skip)] - public static PersonDto ToDto(Person entity) => entity == null ? null : new() + public static PersonDto ToDto(Person entity) => entity == null ? null! : new() { Id = entity.PersonId, FirstName = entity.FirstName, @@ -23,7 +23,7 @@ public static partial class PersonMapper // DTO to entity mapping with expression-bodied syntax //[Updatable] - [Expressive] + [Projectable] public static Person ToEntity(PersonDto dto) => new() { PersonId = dto.Id, @@ -62,4 +62,4 @@ private static void UpdateEntity(Person entity, string firstName, string lastNam entity.EmailAddress = email; entity.BirthDate = birthDate; } -} \ No newline at end of file +} diff --git a/global.json b/global.json index 802ab21..786b012 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,10 @@ { + "sdk": { + "version": "10.0.400", + "rollForward": "latestFeature", + "allowPrerelease": false + }, "test": { "runner": "Microsoft.Testing.Platform" } -} \ No newline at end of file +} diff --git a/source/Adaptation/AdaptationMemberPlanner.cs b/source/Adaptation/AdaptationMemberPlanner.cs index 5cbe9d0..ce5d036 100644 --- a/source/Adaptation/AdaptationMemberPlanner.cs +++ b/source/Adaptation/AdaptationMemberPlanner.cs @@ -26,7 +26,8 @@ public AdaptationMemberPlanner(INamedTypeSymbol mapperType) .Where(m => m.MethodKind == MethodKind.Ordinary) .Select(m => MethodSignature.Build( m.Name, - m.Parameters.Select(p => TypeDisplay.ForSymbol(p.Type, p.NullableAnnotation, NullableContext.Disabled)))), + m.Parameters.Select(p => TypeDisplay.ForSymbol(p.Type, p.NullableAnnotation, NullablePolicy.Disabled)), + m.TypeParameters.Length)), StringComparer.Ordinal); _existingNonMethodNames = new HashSet( diff --git a/source/Adaptation/AdaptationValidator.cs b/source/Adaptation/AdaptationValidator.cs index 82ff21d..f5dfc2f 100644 --- a/source/Adaptation/AdaptationValidator.cs +++ b/source/Adaptation/AdaptationValidator.cs @@ -1,6 +1,7 @@ -#nullable enable +#nullable enable using AlephMapper.Diagnostics; +using AlephMapper.Generation; using AlephMapper.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -17,12 +18,12 @@ namespace AlephMapper.Adaptation; internal static class AdaptationValidator { public static bool Validate( - SourceProductionContext context, - MappingModel mapping, - AdaptationModel adaptation, + MapperGenerationContext context, + MappingAnalysis mapping, + AdaptationAnalysis adaptation, ExpressionSyntax inlinedBody) { - var location = adaptation.Attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() + var location = adaptation.Location?.ToLocation() ?? mapping.MethodSymbol.Locations.FirstOrDefault(); var isValid = true; @@ -97,7 +98,7 @@ public static bool Validate( if (TryGetDirectSourcePath(assignment.Expression, mapping.SemanticModel, mapping.Parameters[0], out var path) && TryResolveReadablePath(adaptation.SourceType, path, out var sourceMember) && - !IsImplicitlyConvertible(mapping.SemanticModel.Compilation, GetMemberType(sourceMember), GetMemberType(destinationMember))) + !IsImplicitlyConvertible(mapping.SemanticModel.Compilation, GetMemberType(sourceMember!), GetMemberType(destinationMember))) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.AdaptIncompatibleType, diff --git a/source/Adaptation/AdaptedDestinationRewriter.cs b/source/Adaptation/AdaptedDestinationRewriter.cs index 5575afd..f4e0724 100644 --- a/source/Adaptation/AdaptedDestinationRewriter.cs +++ b/source/Adaptation/AdaptedDestinationRewriter.cs @@ -4,7 +4,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using AlephMapper.SyntaxRewriters; using AlephMapper.Helpers; using System; using System.Collections.Generic; @@ -22,7 +21,7 @@ internal sealed class AdaptedDestinationRewriter : CSharpSyntaxRewriter private readonly HashSet _destinationCreationTypeTexts; private readonly TypeSyntax _adaptedDestinationType; private readonly ITypeSymbol _adaptedDestinationTypeSymbol; - private readonly NullableContext _nullableContext; + private readonly NullablePolicy _nullablePolicy; private readonly HashSet _originalDestinationTypeTexts; private readonly ExpressionSyntax _root; private readonly Stack _objectInitializerTypeStack = new(); @@ -34,14 +33,14 @@ private AdaptedDestinationRewriter( IEnumerable originalDestinationTypeTexts, string adaptedDestinationTypeName, ITypeSymbol adaptedDestinationTypeSymbol, - NullableContext nullableContext, + NullablePolicy nullablePolicy, ExpressionSyntax root) { _destinationCreationSpans = new HashSet(destinationCreationSpans); _destinationCreationTypeTexts = new HashSet(destinationCreationTypeTexts); _adaptedDestinationType = SyntaxFactory.ParseTypeName(adaptedDestinationTypeName); _adaptedDestinationTypeSymbol = adaptedDestinationTypeSymbol; - _nullableContext = nullableContext; + _nullablePolicy = nullablePolicy; _originalDestinationTypeTexts = new HashSet(originalDestinationTypeTexts); _root = root; } @@ -53,7 +52,7 @@ public static ExpressionSyntax Rewrite( ITypeSymbol originalDestinationType, string adaptedDestinationTypeName, ITypeSymbol adaptedDestinationType, - NullableContext nullableContext) + NullablePolicy nullablePolicy) { if (bodyToRewrite is ImplicitObjectCreationExpressionSyntax implicitCreation) { @@ -89,7 +88,7 @@ public static ExpressionSyntax Rewrite( originalDestinationTypeTexts, adaptedDestinationTypeName, adaptedDestinationType, - nullableContext, + nullablePolicy, bodyToRewrite) .Visit(bodyToRewrite)!; } @@ -270,7 +269,7 @@ private static IEnumerable GetMembersIncludingBaseTypes(ITypeSymbol typ private string GetObjectCreationTypeName(ITypeSymbol type) { - return TypeDisplay.ForSymbol(type, NullableAnnotation.NotAnnotated, _nullableContext); + return TypeDisplay.ForSymbol(type, NullableAnnotation.NotAnnotated, _nullablePolicy); } private string GetCastTypeName(ITypeSymbol type) @@ -278,7 +277,7 @@ private string GetCastTypeName(ITypeSymbol type) var annotation = type.NullableAnnotation == NullableAnnotation.Annotated ? NullableAnnotation.Annotated : NullableAnnotation.NotAnnotated; - return TypeDisplay.ForSymbol(type, annotation, _nullableContext); + return TypeDisplay.ForSymbol(type, annotation, _nullablePolicy); } private static bool IsAdaptableObjectType(ITypeSymbol type) @@ -290,10 +289,12 @@ private static bool IsAdaptableObjectType(ITypeSymbol type) private static HashSet GetTypeTextCandidates(ITypeSymbol type) { + var fullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var candidates = new HashSet(System.StringComparer.Ordinal) { type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), - type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", string.Empty) + fullyQualifiedName, + fullyQualifiedName.Replace("global::", string.Empty) }; if (type is INamedTypeSymbol { TypeArguments.Length: 0 } namedType) @@ -303,7 +304,8 @@ private static HashSet GetTypeTextCandidates(ITypeSymbol type) } candidates.Add(type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) + "?"); - candidates.Add(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", string.Empty) + "?"); + candidates.Add(fullyQualifiedName + "?"); + candidates.Add(fullyQualifiedName.Replace("global::", string.Empty) + "?"); return candidates; } diff --git a/source/AlephMapper.csproj b/source/AlephMapper.csproj index 4e2e58c..7a10a4c 100644 --- a/source/AlephMapper.csproj +++ b/source/AlephMapper.csproj @@ -6,7 +6,6 @@ Your manual mapping companion. true true - true false true latest @@ -14,16 +13,21 @@ MIT https://github.com/Raffinert/AlephMapper README.md - 0.6.2 + 0.7.0 + 0 + $(PackageVersion) + $(PackageVersion).$(BuildNumber) + $(AssemblyVersion) Mapping Companion git https://github.com/Raffinert/AlephMapper.git + true netstandard2.0 Aleph Mapper - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -33,6 +37,7 @@ + diff --git a/source/AlephSourceGenerator.cs b/source/AlephSourceGenerator.cs index 7dec66a..81c1de5 100644 --- a/source/AlephSourceGenerator.cs +++ b/source/AlephSourceGenerator.cs @@ -12,13 +12,65 @@ public sealed class AlephSourceGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(AttributeSourceEmitter.AddAttributes); + context.RegisterPostInitializationOutput(static postInitializationContext => + { + postInitializationContext.AddEmbeddedAttributeDefinition(); + AttributeSourceEmitter.AddAttributes(postInitializationContext); + }); - var mappings = context.SyntaxProvider - .CreateSyntaxProvider(MappingMethodCandidate.IsCandidate, MappingModelFactory.Create) - .Where(static mapping => mapping != null) - .Select(static (mapping, _) => mapping!); + var projectableMappers = context.SyntaxProvider.ForAttributeWithMetadataName( + typeof(ProjectableAttribute).FullName, + MapperCandidate.IsAttributeTarget, + static (attributeContext, cancellationToken) => MapperSourceOutput.Create( + attributeContext, + MapperAttributeKind.Projectable, + cancellationToken)); - context.RegisterSourceOutput(mappings.Collect(), MapperSourceOutput.Generate); + var updatableMappers = context.SyntaxProvider.ForAttributeWithMetadataName( + typeof(UpdatableAttribute).FullName, + MapperCandidate.IsAttributeTarget, + static (attributeContext, cancellationToken) => MapperSourceOutput.Create( + attributeContext, + MapperAttributeKind.Updatable, + cancellationToken)); + + var adaptableMappers = context.SyntaxProvider.ForAttributeWithMetadataName( + typeof(AdaptAttribute).FullName, + MapperCandidate.IsAttributeTarget, + static (attributeContext, cancellationToken) => MapperSourceOutput.Create( + attributeContext, + MapperAttributeKind.Adapt, + cancellationToken)); + + RegisterMapperOutput(context, projectableMappers, "Projectable"); + RegisterMapperOutput(context, updatableMappers, "Updatable"); + RegisterMapperOutput(context, adaptableMappers, "Adapt"); + } + + private static void RegisterMapperOutput( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider mapperResults, + string configurationKind) + { + var results = mapperResults + .WithTrackingName($"AlephMapper.{configurationKind}GenerationResult"); + + var sources = results + .Select(static (result, _) => new MapperSourceResult(result.HintName, result.Source)) + .WithTrackingName($"AlephMapper.{configurationKind}SourceOutput"); + + context.RegisterSourceOutput( + sources, + MapperSourceOutput.EmitSource); + + context.RegisterSourceOutput( + results + .Where(static result => !result.Diagnostics.IsDefaultOrEmpty) + .Combine(context.CompilationProvider) + .WithTrackingName($"AlephMapper.{configurationKind}Diagnostics"), + static (sourceProductionContext, output) => MapperSourceOutput.EmitDiagnostics( + sourceProductionContext, + output.Left, + output.Right)); } } diff --git a/source/AnalyzerReleases.Unshipped.md b/source/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..bc7d19a --- /dev/null +++ b/source/AnalyzerReleases.Unshipped.md @@ -0,0 +1,24 @@ +## Unshipped + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +AM0001 | AlephMapper | Warning | Updatable method has a value-type return. +AM0002 | AlephMapper | Warning | Projectable method has circular references. +AM0003 | AlephMapper | Warning | Updatable method has circular references. +AM0004 | AlephMapper | Error | Source generator crashed. +AM0005 | AlephMapper | Error | Adaptation has an invalid source or destination type. +AM0006 | AlephMapper | Error | Adaptation source member is missing. +AM0007 | AlephMapper | Error | Adaptation destination member is missing or not writable. +AM0008 | AlephMapper | Error | Adaptation has an incompatible type. +AM0009 | AlephMapper | Error | Adapted generated member conflicts with an existing member. +AM0010 | AlephMapper | Warning | Adaptation uses unsupported syntax. +AM0011 | AlephMapper | Error | Adaptation expression generation has no name. +AM0012 | AlephMapper | Error | Adaptation duplicates an explicit type pair. +AM0013 | AlephMapper | Warning | Adaptation has a circular helper reference. +AM0014 | AlephMapper | Error | Adaptation uses an open generic type. +AM0015 | AlephMapper | Error | Adapted generated member fails Roslyn rebinding. +AM0016 | AlephMapper | Warning | Null-conditional receiver cannot be safely rewritten. +AM0017 | AlephMapper | Warning | Null-conditional access is unsupported in expression trees. +AM0018 | AlephMapper | Warning | Adaptation of generic mapping methods is unsupported. diff --git a/source/Attributes.cs b/source/Attributes.cs index 5e01a87..e43bd06 100644 --- a/source/Attributes.cs +++ b/source/Attributes.cs @@ -1,11 +1,14 @@ -using System; +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +34,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +49,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +62,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +85,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +112,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +128,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/source/CodeGenerators/UpdatableMethodGenerator.cs b/source/CodeGenerators/UpdatableMethodGenerator.cs index fe6da7d..0573861 100644 --- a/source/CodeGenerators/UpdatableMethodGenerator.cs +++ b/source/CodeGenerators/UpdatableMethodGenerator.cs @@ -7,16 +7,15 @@ namespace AlephMapper.CodeGenerators; -internal sealed class UpdatableMethodGenerator(string destPrefix, PropertyMappingContext typeContext, IReadOnlyList sourceParamNames) +internal sealed class UpdatableMethodGenerator( + string destPrefix, + PropertyMappingContext typeContext, + IReadOnlyList sourceParamNames, + AlephMapper.Helpers.NullablePolicy nullablePolicy) { private readonly List _lines = []; private readonly string _primarySourceParamName = sourceParamNames.FirstOrDefault() ?? "source"; - private static readonly SymbolDisplayFormat MinimallyQualifiedFormatWithoutNullability = - SymbolDisplayFormat.MinimallyQualifiedFormat.WithMiscellaneousOptions( - SymbolDisplayFormat.MinimallyQualifiedFormat.MiscellaneousOptions - & ~SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - public List ProcessObjectCreation(ObjectCreationExpressionSyntax objectCreation) { _lines.Clear(); @@ -26,7 +25,7 @@ public List ProcessObjectCreation(ObjectCreationExpressionSyntax objectC if (typeContext.ShouldPropertyBePreCreated(destPrefix, out var typeInfo)) { _lines.Add($"if ({destPrefix} == null)"); - _lines.Add($" {destPrefix} = new {typeInfo.Type.ToDisplayString(MinimallyQualifiedFormatWithoutNullability)}();"); + _lines.Add($" {destPrefix} = new {GetTypeName(typeInfo.Type)}();"); } foreach (var expr in objectCreation.Initializer.Expressions) @@ -81,7 +80,7 @@ private void ProcessExpression(ExpressionSyntax expression, string fullDestPath) if (typeContext.ShouldPropertyBePreCreated(fullDestPath, out var typeInfo)) { _lines.Add($"if ({fullDestPath} == null)"); - _lines.Add($" {fullDestPath} = new {typeInfo.Type.ToDisplayString(MinimallyQualifiedFormatWithoutNullability)}();"); + _lines.Add($" {fullDestPath} = new {GetTypeName(typeInfo.Type)}();"); } _lines.Add($"{fullDestPath} = {NormalizeConditionalMemberAccess(expression)};"); } @@ -177,7 +176,7 @@ private void ProcessObjectCreationWithIndent(ObjectCreationExpressionSyntax obje if (typeContext.ShouldPropertyBePreCreated(fullDestPath, out var typeInfo)) { _lines.Add($"{indent}if ({fullDestPath} == null)"); - _lines.Add($"{indent} {fullDestPath} = new {typeInfo.Type.ToDisplayString(MinimallyQualifiedFormatWithoutNullability)}();"); + _lines.Add($"{indent} {fullDestPath} = new {GetTypeName(typeInfo.Type)}();"); } // Process nested properties @@ -280,7 +279,7 @@ private void ProcessNestedObjectCreationInBranch(ObjectCreationExpressionSyntax if (typeContext.ShouldPropertyBePreCreated(fullDestPath, out var typeInfo)) { lines.Add($"{indent}if ({fullDestPath} == null)"); - lines.Add($"{indent} {fullDestPath} = new {typeInfo.Type.ToDisplayString(MinimallyQualifiedFormatWithoutNullability)}();"); + lines.Add($"{indent} {fullDestPath} = new {GetTypeName(typeInfo.Type)}();"); } // Process nested properties @@ -312,7 +311,7 @@ private void ProcessNestedObjectCreation(ObjectCreationExpressionSyntax objectCr if (typeContext.ShouldPropertyBePreCreated(fullDestPath, out var typeInfo)) { lines.Add($"{indent}if ({fullDestPath} == null)"); - lines.Add($"{indent} {fullDestPath} = new {typeInfo.Type.ToDisplayString(MinimallyQualifiedFormatWithoutNullability)}();"); + lines.Add($"{indent} {fullDestPath} = new {GetTypeName(typeInfo.Type)}();"); } @@ -354,7 +353,7 @@ private void ProcessDirectObjectCreation(ObjectCreationExpressionSyntax objectCr if (typeContext.ShouldPropertyBePreCreated(fullDestPath, out var typeInfo)) { _lines.Add($"if ({fullDestPath} == null)"); - _lines.Add($" {fullDestPath} = new {typeInfo.Type.ToDisplayString(MinimallyQualifiedFormatWithoutNullability)}();"); + _lines.Add($" {fullDestPath} = new {GetTypeName(typeInfo.Type)}();"); } if (objectCreation.Initializer?.Expressions != null) @@ -412,6 +411,12 @@ private static bool IsNullExpression(ExpressionSyntax expression) return expression?.ToString().Trim() == "null"; } + private string GetTypeName(ITypeSymbol type) => + AlephMapper.Helpers.TypeDisplay.ForSymbol( + type, + NullableAnnotation.NotAnnotated, + nullablePolicy); + private string NormalizeConditionalMemberAccess(ExpressionSyntax expression) { // Convert shapes like (conditional)?.WhenNotNull.Tail into a textual form that preserves the diff --git a/source/Diagnostics/CrashDiagnosticsReporter.cs b/source/Diagnostics/CrashDiagnosticsReporter.cs index 065dbe1..e260908 100644 --- a/source/Diagnostics/CrashDiagnosticsReporter.cs +++ b/source/Diagnostics/CrashDiagnosticsReporter.cs @@ -1,4 +1,5 @@ using System; +using AlephMapper.Generation; using Microsoft.CodeAnalysis; namespace AlephMapper.Diagnostics; @@ -9,18 +10,12 @@ internal static class CrashDiagnosticsReporter { private const int MaxCrashDiagnosticLength = 2_000; - internal static void Report( - in SourceProductionContext sourceProductionContext, - Exception exception - ) + internal static GenerationDiagnostic CreateDiagnostic(Exception exception) { - sourceProductionContext.ReportDiagnostic( - Diagnostic.Create( - DiagnosticDescriptors.GeneratorCrash, - Location.None, - FormatCrashDiagnostic(exception) - ) - ); + return Generation.GenerationDiagnostic.From(Diagnostic.Create( + DiagnosticDescriptors.GeneratorCrash, + Location.None, + FormatCrashDiagnostic(exception))); } private static string FormatCrashDiagnostic(Exception exception) @@ -32,6 +27,6 @@ private static string FormatCrashDiagnostic(Exception exception) return details; } - return $"{details.Substring(MaxCrashDiagnosticLength)}..."; + return $"{details.Substring(0, MaxCrashDiagnosticLength)}..."; } } diff --git a/source/Diagnostics/DiagnosticDescriptors.cs b/source/Diagnostics/DiagnosticDescriptors.cs index 31d8874..32d31cc 100644 --- a/source/Diagnostics/DiagnosticDescriptors.cs +++ b/source/Diagnostics/DiagnosticDescriptors.cs @@ -1,3 +1,5 @@ +#nullable enable + using Microsoft.CodeAnalysis; namespace AlephMapper.Diagnostics; @@ -7,7 +9,7 @@ namespace AlephMapper.Diagnostics; public static class DiagnosticDescriptors { private const string CrashIssueUrl = - "https://github.com/Raffinert/AlephMapper/issues/new?labels=bug&title=Generator%20crash:%20IMP005"; + "https://github.com/Raffinert/AlephMapper/issues/new?labels=bug&title=Generator%20crash:%20AM0004"; public static readonly DiagnosticDescriptor UpdatableValueTypeReturn = new( "AM0001", @@ -18,9 +20,9 @@ public static class DiagnosticDescriptors isEnabledByDefault: true ); - public static readonly DiagnosticDescriptor ExpressiveCircularReferences = new( + public static readonly DiagnosticDescriptor ProjectableCircularReferences = new( "AM0002", - "Expressive method generation skipped due to circular references", + "Projectable method generation skipped due to circular references", "Expression method generation skipped for '{0}' due to circular references. Fix the circular dependencies to enable expression generation.", "AlephMapper", DiagnosticSeverity.Warning, @@ -50,7 +52,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor InvalidAdaptType = new( "AM0005", "Invalid adapted source or destination type", - "Adaptation for method '{0}' has an invalid source or destination type.", + "Adaptation for method '{0}' has an invalid source or destination type", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -58,7 +60,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptSourceMemberMissing = new( "AM0006", "Required adapted source member is missing", - "Cannot adapt '{0}': source member path '{1}' cannot be resolved on '{2}'.", + "Cannot adapt '{0}': source member path '{1}' cannot be resolved on '{2}'", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -66,7 +68,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptDestinationMemberMissing = new( "AM0007", "Required adapted destination member is missing or not writable", - "Cannot adapt '{0}': destination member '{1}' is missing or not writable on '{2}'.", + "Cannot adapt '{0}': destination member '{1}' is missing or not writable on '{2}'", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -74,7 +76,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptIncompatibleType = new( "AM0008", "Adapted expression or assignment has an incompatible type", - "Cannot adapt '{0}': adapted expression or assignment is not type-compatible for '{1}'.", + "Cannot adapt '{0}': adapted expression or assignment is not type-compatible for '{1}'", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -82,7 +84,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptNameConflict = new( "AM0009", "Generated adapted method name or signature conflicts", - "Cannot adapt '{0}': generated member name or signature '{1}' conflicts with an existing or generated member.", + "Cannot adapt '{0}': generated member name or signature '{1}' conflicts with an existing or generated member", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -90,7 +92,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptUnsupportedSyntax = new( "AM0010", "Template contains unsupported adaptation syntax", - "Cannot adapt '{0}': the template contains unsupported syntax '{1}'.", + "Cannot adapt '{0}': the template contains unsupported syntax '{1}'", "AlephMapper", DiagnosticSeverity.Warning, isEnabledByDefault: true); @@ -98,7 +100,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptExpressionWithoutName = new( "AM0011", "Expression generation requested without a generated name", - "Cannot adapt '{0}': Name is required when Generate includes Expression.", + "Cannot adapt '{0}': Name is required when Generate includes Expression", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -106,7 +108,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptDuplicatePair = new( "AM0012", "Duplicate adaptation for the same explicit type pair", - "Cannot adapt '{0}': duplicate adaptation for source '{1}' and destination '{2}'.", + "Cannot adapt '{0}': duplicate adaptation for source '{1}' and destination '{2}'", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -114,7 +116,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptCircularHelper = new( "AM0013", "Circular helper reference prevents adaptation", - "Adaptation skipped for '{0}' due to circular helper references.", + "Adaptation skipped for '{0}' due to circular helper references", "AlephMapper", DiagnosticSeverity.Warning, isEnabledByDefault: true); @@ -122,7 +124,7 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor AdaptOpenGenericType = new( "AM0014", "Adapted source or destination type is open generic", - "Cannot adapt '{0}': adapted source or destination type must not be open generic.", + "Cannot adapt '{0}': adapted source or destination type must not be open generic", "AlephMapper", DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -152,4 +154,35 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Select NullConditionalRewrite.Ignore or NullConditionalRewrite.Rewrite to generate an expression-tree-compatible companion."); + + public static readonly DiagnosticDescriptor AdaptGenericMethodUnsupported = new( + "AM0018", + "Adaptation of generic mapping methods is unsupported", + "Cannot adapt '{0}': Adapt does not support generic mapping methods. Use the generic Projectable or Updatable companion directly.", + "AlephMapper", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static DiagnosticDescriptor? GetById(string id) => id switch + { + "AM0001" => UpdatableValueTypeReturn, + "AM0002" => ProjectableCircularReferences, + "AM0003" => UpdatableCircularReferences, + "AM0004" => GeneratorCrash, + "AM0005" => InvalidAdaptType, + "AM0006" => AdaptSourceMemberMissing, + "AM0007" => AdaptDestinationMemberMissing, + "AM0008" => AdaptIncompatibleType, + "AM0009" => AdaptNameConflict, + "AM0010" => AdaptUnsupportedSyntax, + "AM0011" => AdaptExpressionWithoutName, + "AM0012" => AdaptDuplicatePair, + "AM0013" => AdaptCircularHelper, + "AM0014" => AdaptOpenGenericType, + "AM0015" => AdaptRebindingFailed, + "AM0016" => UnsafeNullConditionalReceiver, + "AM0017" => UnsupportedNullConditionalExpression, + "AM0018" => AdaptGenericMethodUnsupported, + _ => null + }; } diff --git a/source/EmbeddedAttribute.cs b/source/EmbeddedAttribute.cs new file mode 100644 index 0000000..ef21364 --- /dev/null +++ b/source/EmbeddedAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Microsoft.CodeAnalysis; + +// The generator host injects the real definition into consuming compilations +// through AddEmbeddedAttributeDefinition. This shim only lets Attributes.cs be +// compiled into the generator assembly before it is embedded as source. +[AttributeUsage(AttributeTargets.All)] +internal sealed class EmbeddedAttribute : Attribute +{ +} diff --git a/source/GeneratedSyntaxAnnotations.cs b/source/GeneratedSyntaxAnnotations.cs new file mode 100644 index 0000000..5ddc287 --- /dev/null +++ b/source/GeneratedSyntaxAnnotations.cs @@ -0,0 +1,7 @@ +namespace AlephMapper; + +internal static class GeneratedSyntaxAnnotations +{ + internal const string MultilineConditional = "AlephMapper.MultilineConditional"; + internal const string SingleLineConditional = "AlephMapper.SingleLineConditional"; +} diff --git a/source/Generation/AttributeSourceEmitter.cs b/source/Generation/AttributeSourceEmitter.cs index c43bde8..a23686e 100644 --- a/source/Generation/AttributeSourceEmitter.cs +++ b/source/Generation/AttributeSourceEmitter.cs @@ -11,6 +11,7 @@ public static void AddAttributes(IncrementalGeneratorPostInitializationContext c { var assembly = typeof(AlephSourceGenerator).Assembly; using var reader = new StreamReader(assembly.GetManifestResourceStream("AlephMapper.Attributes.cs")!); - context.AddSource("AlephMapper.Attributes.g.cs", SourceText.From(reader.ReadToEnd(), Encoding.UTF8)); + var source = "// \n#nullable enable\n\n" + reader.ReadToEnd(); + context.AddSource("AlephMapper.Attributes.g.cs", SourceText.From(source, Encoding.UTF8)); } } diff --git a/source/Generation/Emitters/AdaptationMemberEmitter.cs b/source/Generation/Emitters/AdaptationMemberEmitter.cs index 15deceb..af6c823 100644 --- a/source/Generation/Emitters/AdaptationMemberEmitter.cs +++ b/source/Generation/Emitters/AdaptationMemberEmitter.cs @@ -18,6 +18,19 @@ internal static class AdaptationMemberEmitter public static void Emit(MappingMethodDetails details, MapperGenerationContext context) { var mapping = details.Mapping; + if (mapping.MethodSymbol.TypeParameters.Length != 0) + { + foreach (var adaptation in mapping.Adaptations) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.AdaptGenericMethodUnsupported, + GetLocation(mapping, adaptation), + mapping.MethodSymbol.Name)); + } + + return; + } + var adaptationPairs = new HashSet(StringComparer.Ordinal); foreach (var adaptation in mapping.Adaptations) { @@ -26,7 +39,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co var generateUpdate = (adaptation.Generation & AdaptGeneration.Update) == AdaptGeneration.Update; if (generateExpression && string.IsNullOrWhiteSpace(adaptation.GeneratedName)) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.AdaptExpressionWithoutName, GetLocation(mapping, adaptation), mapping.MethodSymbol.Name)); @@ -34,17 +47,17 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co } var adaptationName = string.IsNullOrWhiteSpace(adaptation.GeneratedName) ? mapping.Name : adaptation.GeneratedName!; - var sourceTypeName = TypeDisplay.ForSymbol(adaptation.SourceType, NullableAnnotation.None, details.NullableContext); - var destinationTypeName = TypeDisplay.ForSymbol(adaptation.DestinationType, NullableAnnotation.None, details.NullableContext); + var sourceTypeName = TypeDisplay.ForSymbol(adaptation.SourceType, NullableAnnotation.None, details.NullablePolicy); + var destinationTypeName = TypeDisplay.ForSymbol(adaptation.DestinationType, NullableAnnotation.None, details.NullablePolicy); var additionalParametersWithNames = string.Join(", ", mapping.Parameters.Skip(1).Select(parameter => - $"{TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, details.NullableContext)} {parameter.Name}")); + $"{TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, details.NullablePolicy)} {parameter.Name}")); var adaptationParametersWithNames = sourceTypeName + " " + details.SourceName + (string.IsNullOrEmpty(additionalParametersWithNames) ? "" : ", " + additionalParametersWithNames); var pairSignature = MethodSignature.Build("", [sourceTypeName, destinationTypeName]); if (!adaptationPairs.Add(pairSignature)) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.AdaptDuplicatePair, GetLocation(mapping, adaptation), mapping.MethodSymbol.Name, @@ -53,12 +66,14 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co continue; } - var inliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, false, adaptation.NullStrategy); - var inlinedBody = (ExpressionSyntax)inliner.Visit(mapping.BodySyntax.Expression)!.WithoutTrivia(); + var inliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, false, adaptation.NullStrategy, details.NullablePolicy); + var inlinedBody = ((ExpressionSyntax)inliner.Visit(mapping.BodySyntax.Expression)!) + .WithoutLeadingTrivia() + .WithoutTrailingTrivia(); context.AddUsings(inliner.UsingDirectives.Concat(mapping.UsingDirectives)); if (inliner.CircularReferences.Any()) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.AdaptCircularHelper, mapping.MethodSymbol.Locations.FirstOrDefault(), mapping.MethodSymbol.Name)); @@ -68,7 +83,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co if ((generateMap || generateExpression) && inliner.UnsafeConditionalReceivers.FirstOrDefault() is { } unsafeReceiver) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UnsafeNullConditionalReceiver, unsafeReceiver.Location, mapping.MethodSymbol.Name, @@ -79,7 +94,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co if (generateExpression && inliner.UnsupportedNullConditionals.FirstOrDefault() is { } unsupportedConditional) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UnsupportedNullConditionalExpression, unsupportedConditional.Location, mapping.MethodSymbol.Name, @@ -87,7 +102,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co continue; } - if (!AdaptationValidator.Validate(context.SourceProductionContext, mapping, adaptation, inlinedBody)) + if (!AdaptationValidator.Validate(context, mapping, adaptation, inlinedBody)) { continue; } @@ -99,7 +114,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co mapping.ReturnType, destinationTypeName, adaptation.DestinationType, - details.NullableContext); + details.NullablePolicy); var adaptedBodyText = PrettyPrinter.Print(adaptedBody, 2); var adaptationParameterTypes = new[] { sourceTypeName }.Concat(details.ParameterTypeNames.Skip(1)).ToArray(); @@ -122,7 +137,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co out var conflict); if (generatedConflict || plannerConflict) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.AdaptNameConflict, GetLocation(mapping, adaptation), mapping.MethodSymbol.Name, @@ -153,7 +168,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co { if (adaptation.DestinationType.IsValueType && !SymbolHelpers.CanBeNull(adaptation.DestinationType)) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UpdatableValueTypeReturn, GetLocation(mapping, adaptation), adaptationName, @@ -161,12 +176,12 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co continue; } - var updateInliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, true, NullConditionalRewrite.None); + var updateInliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, true, NullConditionalRewrite.None, details.NullablePolicy); var inlinedUpdateBody = (ExpressionSyntax)updateInliner.Visit(mapping.BodySyntax.Expression)!.WithoutTrivia(); context.AddUsings(updateInliner.UsingDirectives.Concat(mapping.UsingDirectives)); if (updateInliner.CircularReferences.Any()) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UpdatableCircularReferences, GetLocation(mapping, adaptation), adaptationName)); @@ -180,7 +195,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co mapping.ReturnType, destinationTypeName, adaptation.DestinationType, - details.NullableContext); + details.NullablePolicy); var lines = new List(); if (!EmitHelpers.TryBuildUpdateAssignmentsWithInlining( adaptedUpdateBody, @@ -189,6 +204,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co adaptation.SourceType, mapping.Parameters.Select(parameter => parameter.Name).ToArray(), mapping.CollectionPolicy, + details.NullablePolicy, lines)) { continue; @@ -214,7 +230,7 @@ private static void EmitMapMember( string adaptedBodyText, MapperGenerationContext context) { - context.AppendMember(members => + context.AppendMember(details.NullablePolicy, members => { members.AppendLine(" /// "); members.AppendLine($" /// This is an auto-generated adapted mapping method for ."); @@ -231,7 +247,7 @@ private static void EmitExpressionMember( string adaptedBodyText, MapperGenerationContext context) { - context.AppendMember(members => + context.AppendMember(details.NullablePolicy, members => { members.AppendLine(" /// "); members.AppendLine($" /// This is an auto-generated adapted expression companion for ."); @@ -239,7 +255,7 @@ private static void EmitExpressionMember( var expressionMethodParameters = string.IsNullOrEmpty(details.ExtraExpressionParameterListWithNames) ? "()" : "(" + details.ExtraExpressionParameterListWithNames + ")"; - members.AppendLine(" public static Expression> " + expressionName + expressionMethodParameters + " => "); + members.AppendLine(" public static global::System.Linq.Expressions.Expression> " + expressionName + expressionMethodParameters + " =>"); members.Append(" " + details.ProjectionLambdaParameter + " => "); members.AppendLine(adaptedBodyText + ";"); }); @@ -253,7 +269,7 @@ private static void EmitUpdateMember( IEnumerable lines, MapperGenerationContext context) { - context.AppendMember(members => + context.AppendMember(details.NullablePolicy, members => { members.AppendLine(" /// "); members.AppendLine($" /// This is an auto-generated adapted update method for ."); @@ -268,9 +284,9 @@ private static void EmitUpdateMember( }); } - private static Location? GetLocation(MappingModel mapping, AdaptationModel adaptation) + private static Location? GetLocation(MappingAnalysis mapping, AdaptationAnalysis adaptation) { - return adaptation.Attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? + return adaptation.Location?.ToLocation() ?? mapping.MethodSymbol.Locations.FirstOrDefault(); } } diff --git a/source/Generation/Emitters/ExpressiveMemberEmitter.cs b/source/Generation/Emitters/ProjectableMemberEmitter.cs similarity index 69% rename from source/Generation/Emitters/ExpressiveMemberEmitter.cs rename to source/Generation/Emitters/ProjectableMemberEmitter.cs index 250cef2..a5b16e7 100644 --- a/source/Generation/Emitters/ExpressiveMemberEmitter.cs +++ b/source/Generation/Emitters/ProjectableMemberEmitter.cs @@ -5,24 +5,26 @@ namespace AlephMapper.Generation.Emitters; -internal static class ExpressiveMemberEmitter +internal static class ProjectableMemberEmitter { public static void Emit(MappingMethodDetails details, MapperGenerationContext context) { var mapping = details.Mapping; - if (!mapping.IsExpressive) + if (!mapping.IsProjectable) { return; } - var inliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, false, mapping.NullStrategy); - var inlinedBody = inliner.Visit(mapping.BodySyntax.Expression)!.WithoutTrivia(); + var inliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, false, mapping.NullStrategy, details.NullablePolicy); + var inlinedBody = inliner.Visit(mapping.BodySyntax.Expression)! + .WithoutLeadingTrivia() + .WithoutTrailingTrivia(); context.AddUsings(inliner.UsingDirectives.Concat(mapping.UsingDirectives)); if (inliner.CircularReferences.Any()) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.ExpressiveCircularReferences, + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.ProjectableCircularReferences, mapping.MethodSymbol.Locations.FirstOrDefault(), mapping.MethodSymbol.Name)); return; @@ -30,7 +32,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co if (inliner.UnsafeConditionalReceivers.FirstOrDefault() is { } unsafeReceiver) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UnsafeNullConditionalReceiver, unsafeReceiver.Location, mapping.MethodSymbol.Name, @@ -40,7 +42,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co if (inliner.UnsupportedNullConditionals.FirstOrDefault() is { } unsupportedConditional) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UnsupportedNullConditionalExpression, unsupportedConditional.Location, mapping.MethodSymbol.Name, @@ -49,7 +51,10 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co } var expressionMethodName = mapping.Name + "Expression"; - context.GeneratedMemberSignatures.Add(MethodSignature.Build(expressionMethodName, details.ExtraExpressionParameterTypeNames)); + context.GeneratedMemberSignatures.Add(MethodSignature.Build( + expressionMethodName, + details.ExtraExpressionParameterTypeNames, + details.MethodTypeParameterCount)); var nullStrategyDescription = mapping.NullStrategy switch { NullConditionalRewrite.None => "Null-conditional operators are preserved as-is in the expression tree.", @@ -63,7 +68,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co ? "()" : "(" + details.ExtraExpressionParameterListWithNames + ")"; - context.AppendMember(members => + context.AppendMember(details.NullablePolicy, members => { members.AppendLine(" /// "); members.AppendLine($" /// This is an auto-generated expression companion for ."); @@ -73,7 +78,21 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co members.AppendLine($" /// Null handling strategy: {nullStrategyDescription}"); members.AppendLine(" /// "); members.AppendLine(" /// "); - members.AppendLine(" public static Expression> " + expressionMethodName + expressionMethodParameters + " => "); + var declaration = " public static global::System.Linq.Expressions.Expression> " + + expressionMethodName + details.MethodTypeParameterList + expressionMethodParameters; + if (details.MethodConstraintClauses.Count == 0) + { + members.AppendLine(declaration + " =>"); + } + else + { + members.AppendLine(declaration); + foreach (var constraintClause in details.MethodConstraintClauses) + { + members.AppendLine(" " + constraintClause); + } + members.AppendLine(" => "); + } members.Append(" " + details.ProjectionLambdaParameter + " => "); members.AppendLine(prettyBody + ";"); }); diff --git a/source/Generation/Emitters/UpdatableMemberEmitter.cs b/source/Generation/Emitters/UpdatableMemberEmitter.cs index 1567b0d..1367bf7 100644 --- a/source/Generation/Emitters/UpdatableMemberEmitter.cs +++ b/source/Generation/Emitters/UpdatableMemberEmitter.cs @@ -17,12 +17,12 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co return; } - var inliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, true, NullConditionalRewrite.None); + var inliner = new InliningResolver(mapping.SemanticModel, context.MappingsByMethod, true, NullConditionalRewrite.None, details.NullablePolicy); var inlinedBody = inliner.Visit(mapping.BodySyntax.Expression)!.WithoutTrivia(); context.AddUsings(inliner.UsingDirectives.Concat(mapping.UsingDirectives)); if (inliner.CircularReferences.Any()) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UpdatableCircularReferences, mapping.MethodSymbol.Locations.FirstOrDefault(), mapping.MethodSymbol.Name)); @@ -31,7 +31,7 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co if (mapping.ReturnType.IsValueType && !SymbolHelpers.CanBeNull(mapping.ReturnType)) { - context.SourceProductionContext.ReportDiagnostic(Diagnostic.Create( + context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.UpdatableValueTypeReturn, mapping.MethodSymbol.Locations.FirstOrDefault(), mapping.MethodSymbol.Name, @@ -48,12 +48,13 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co mapping.ParamType, mapping.Parameters.Select(parameter => parameter.Name).ToArray(), mapping.CollectionPolicy, + details.NullablePolicy, lines)) { return; } - context.AppendMember(members => + context.AppendMember(details.NullablePolicy, members => { members.AppendLine(" /// "); members.AppendLine($" /// This is an auto-generated update method for ."); @@ -65,7 +66,11 @@ public static void Emit(MappingMethodDetails details, MapperGenerationContext co } members.AppendLine(" /// The destination object to update. If null, the new instance is created."); members.AppendLine(" /// The updated destination object for method chaining, or the new destination instance if either parameter is null."); - members.AppendLine(" public static " + details.DestinationTypeName + " " + mapping.Name + "(" + details.MethodParameterListWithNames + ", " + details.DestinationTypeName + " dest)"); + members.AppendLine(" public static " + details.DestinationTypeName + " " + mapping.Name + details.MethodTypeParameterList + "(" + details.MethodParameterListWithNames + ", " + details.DestinationTypeName + " dest)"); + foreach (var constraintClause in details.MethodConstraintClauses) + { + members.AppendLine(" " + constraintClause); + } members.AppendLine(" {"); foreach (var line in lines) { diff --git a/source/Generation/MapperCandidate.cs b/source/Generation/MapperCandidate.cs new file mode 100644 index 0000000..e7bde50 --- /dev/null +++ b/source/Generation/MapperCandidate.cs @@ -0,0 +1,101 @@ +#nullable enable + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Threading; + +namespace AlephMapper.Generation; + +/// +/// Value-only identity for an AlephMapper attribute target. The target is +/// kept separate from its containing mapper so multiple configuration kinds +/// can be normalized to one generated file without caching a symbol. +/// +internal sealed class MapperCandidate : IEquatable +{ + internal MapperCandidate( + string filePath, + int start, + int length, + MapperAttributeKind attributeKind) + { + FilePath = filePath; + Start = start; + Length = length; + AttributeKind = attributeKind; + } + + public string FilePath { get; } + public int Start { get; } + public int Length { get; } + public MapperAttributeKind AttributeKind { get; } + + public static bool IsAttributeTarget(SyntaxNode node, CancellationToken _) + { + var containingClass = node switch + { + ClassDeclarationSyntax classDeclaration => classDeclaration, + MethodDeclarationSyntax { Parent: ClassDeclarationSyntax classDeclaration } => classDeclaration, + _ => null + }; + + if (containingClass is null) + { + return false; + } + + foreach (var modifier in containingClass.Modifiers) + { + if (modifier.IsKind(SyntaxKind.StaticKeyword)) + { + return true; + } + } + + return false; + } + + public static MapperCandidate Create( + GeneratorAttributeSyntaxContext context, + MapperAttributeKind attributeKind, + CancellationToken _) + { + var targetNode = context.TargetNode; + return new MapperCandidate( + targetNode.SyntaxTree.FilePath, + targetNode.SpanStart, + targetNode.Span.Length, + attributeKind); + } + + public bool Equals(MapperCandidate? other) + { + return other is not null && + Start == other.Start && + Length == other.Length && + AttributeKind == other.AttributeKind && + string.Equals(FilePath, other.FilePath, StringComparison.Ordinal); + } + + public override bool Equals(object? obj) => Equals(obj as MapperCandidate); + + public override int GetHashCode() + { + unchecked + { + var hash = StringComparer.Ordinal.GetHashCode(FilePath); + hash = (hash * 397) ^ Start; + hash = (hash * 397) ^ Length; + return (hash * 397) ^ (int)AttributeKind; + } + } +} + +internal enum MapperAttributeKind +{ + Projectable, + Updatable, + Adapt +} diff --git a/source/Generation/MapperFileEmitter.cs b/source/Generation/MapperFileEmitter.cs index 87e35dd..2caaba9 100644 --- a/source/Generation/MapperFileEmitter.cs +++ b/source/Generation/MapperFileEmitter.cs @@ -11,8 +11,7 @@ internal static class MapperFileEmitter [ "System", "System.Linq", - "System.Linq.Expressions", - "System.CodeDom.Compiler" + "System.Linq.Expressions" ]; public static GeneratedMapperFile Render(MapperGenerationContext context) @@ -24,6 +23,8 @@ public static GeneratedMapperFile Render(MapperGenerationContext context) : ""; var output = new StringBuilder(); + output.AppendLine("// "); + output.AppendLine(); foreach (var usingDirective in context.UsingDirectives.OrderBy(static directive => directive)) { if (usingDirective != containingNamespace && !string.IsNullOrEmpty(usingDirective)) @@ -39,7 +40,7 @@ public static GeneratedMapperFile Render(MapperGenerationContext context) output.AppendLine(); } - output.AppendLine($"[GeneratedCode(\"AlephMapper\", \"{VersionInfo.Version}\")]" ); + output.AppendLine($"[global::System.CodeDom.Compiler.GeneratedCode(\"AlephMapper\", \"{VersionInfo.Version}\")]" ); var containingTypes = GetContainingTypes(mapperType); foreach (var containingType in containingTypes) { diff --git a/source/Generation/MapperGenerationContext.cs b/source/Generation/MapperGenerationContext.cs index 6b4af1e..b872801 100644 --- a/source/Generation/MapperGenerationContext.cs +++ b/source/Generation/MapperGenerationContext.cs @@ -1,8 +1,9 @@ using AlephMapper.Adaptation; -using AlephMapper.Models; +using AlephMapper.Helpers; using Microsoft.CodeAnalysis; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Text; namespace AlephMapper.Generation; @@ -11,42 +12,41 @@ namespace AlephMapper.Generation; /// Holds the mutable state for one generated mapper file. Feature emitters use /// this instead of sharing source-output orchestration concerns. /// -internal sealed class MapperGenerationContext +internal sealed class MapperGenerationContext( + INamedTypeSymbol mapperType, + MappingCatalog mappingsByMethod) { private bool _hasMembers; - public MapperGenerationContext( - INamedTypeSymbol mapperType, - IDictionary mappingsByMethod, - SourceProductionContext sourceProductionContext) - { - MapperType = mapperType; - MappingsByMethod = mappingsByMethod; - SourceProductionContext = sourceProductionContext; - AdaptationMembers = new AdaptationMemberPlanner(mapperType); - } - - public INamedTypeSymbol MapperType { get; } - public IDictionary MappingsByMethod { get; } - public SourceProductionContext SourceProductionContext { get; } - public AdaptationMemberPlanner AdaptationMembers { get; } + public INamedTypeSymbol MapperType { get; } = mapperType; + public MappingCatalog MappingsByMethod { get; } = mappingsByMethod; + public AdaptationMemberPlanner AdaptationMembers { get; } = new(mapperType); public HashSet UsingDirectives { get; } = new(StringComparer.Ordinal); public HashSet GeneratedMemberSignatures { get; } = new(StringComparer.Ordinal); public StringBuilder Members { get; } = new(); + private List Diagnostics { get; } = new(); + + public ImmutableArray GetDiagnostics() => [.. Diagnostics]; + + public void ReportDiagnostic(Diagnostic diagnostic) => Diagnostics.Add(GenerationDiagnostic.From(diagnostic)); public void AddUsings(IEnumerable usingDirectives) { UsingDirectives.UnionWith(usingDirectives); } - public void AppendMember(Action writeMember) + public void AppendMember(NullablePolicy nullablePolicy, Action writeMember) { if (_hasMembers) { Members.AppendLine(); } + Members.AppendLine($"#nullable {nullablePolicy.Directive}"); + writeMember(Members); + Members.AppendLine("#nullable restore"); + _hasMembers = true; } } diff --git a/source/Generation/MapperGenerationResult.cs b/source/Generation/MapperGenerationResult.cs new file mode 100644 index 0000000..c186727 --- /dev/null +++ b/source/Generation/MapperGenerationResult.cs @@ -0,0 +1,137 @@ +#nullable enable + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using AlephMapper.Diagnostics; +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace AlephMapper.Generation; + +/// +/// Value-only output from mapper analysis. This is the boundary between +/// compiler-bound semantic rewriting and incremental source production. +/// +internal sealed class MapperGenerationResult( + string? hintName, + string? source, + ImmutableArray diagnostics) + : IEquatable +{ + public string? HintName { get; } = hintName; + public string? Source { get; } = source; + public ImmutableArray Diagnostics { get; } = diagnostics; + + public static MapperGenerationResult Empty { get; } = new MapperGenerationResult(null, null, ImmutableArray.Empty); + + public bool Equals(MapperGenerationResult? other) + { + return other is not null && + string.Equals(HintName, other.HintName, StringComparison.Ordinal) && + string.Equals(Source, other.Source, StringComparison.Ordinal) && + Diagnostics.SequenceEqual(other.Diagnostics); + } + + public override bool Equals(object? obj) => Equals(obj as MapperGenerationResult); + public override int GetHashCode() => (HintName, Source).GetHashCode(); +} + +internal readonly struct GenerationDiagnostic( + string id, + string title, + string message, + DiagnosticSeverity severity, + string category, + string? filePath, + int start, + int length, + int startLine, + int startCharacter, + int endLine, + int endCharacter) + : IEquatable +{ + public string Id { get; } = id; + public string Title { get; } = title; + public string Message { get; } = message; + public DiagnosticSeverity Severity { get; } = severity; + public string Category { get; } = category; + public string? FilePath { get; } = filePath; + public int Start { get; } = start; + public int Length { get; } = length; + public int StartLine { get; } = startLine; + public int StartCharacter { get; } = startCharacter; + public int EndLine { get; } = endLine; + public int EndCharacter { get; } = endCharacter; + + public static GenerationDiagnostic From(Diagnostic diagnostic) + { + var location = diagnostic.Location; + var lineSpan = location == Location.None ? default : location.GetLineSpan(); + return new GenerationDiagnostic( + diagnostic.Id, + diagnostic.Descriptor.Title.ToString(), + diagnostic.GetMessage(), + diagnostic.Severity, + diagnostic.Descriptor.Category, + location == Location.None ? null : location.SourceTree?.FilePath ?? lineSpan.Path, + location == Location.None ? 0 : location.SourceSpan.Start, + location == Location.None ? 0 : location.SourceSpan.Length, + location == Location.None ? 0 : lineSpan.StartLinePosition.Line, + location == Location.None ? 0 : lineSpan.StartLinePosition.Character, + location == Location.None ? 0 : lineSpan.EndLinePosition.Line, + location == Location.None ? 0 : lineSpan.EndLinePosition.Character); + } + + public Diagnostic ToDiagnostic(Compilation compilation) + { + var descriptor = CreateDescriptor(); + if (string.IsNullOrEmpty(FilePath)) + { + return Diagnostic.Create(descriptor, Location.None); + } + + var span = new TextSpan(Start, Length); + var filePath = FilePath; + var sourceTree = compilation.SyntaxTrees.FirstOrDefault(tree => + string.Equals(tree.FilePath, filePath, StringComparison.Ordinal)); + if (sourceTree is not null && span.End <= sourceTree.GetText().Length) + { + return Diagnostic.Create(descriptor, Location.Create(sourceTree, span)); + } + + var lineSpan = new LinePositionSpan( + new LinePosition(StartLine, StartCharacter), + new LinePosition(EndLine, EndCharacter)); + return Diagnostic.Create(descriptor, Location.Create(FilePath!, span, lineSpan)); + } + + private DiagnosticDescriptor CreateDescriptor() + { + var original = DiagnosticDescriptors.GetById(Id); + return original is null + ? new DiagnosticDescriptor(Id, Title, Message, Category, Severity, isEnabledByDefault: true) + : new DiagnosticDescriptor( + original.Id, + original.Title, + Message, + original.Category, + original.DefaultSeverity, + original.IsEnabledByDefault, + original.Description, + original.HelpLinkUri, + original.CustomTags.ToArray()); + } + + public bool Equals(GenerationDiagnostic other) + { + return Id == other.Id && Title == other.Title && Message == other.Message && Severity == other.Severity && + Category == other.Category && FilePath == other.FilePath && Start == other.Start && Length == other.Length && + StartLine == other.StartLine && StartCharacter == other.StartCharacter && + EndLine == other.EndLine && EndCharacter == other.EndCharacter; + } + + public override bool Equals(object? obj) => obj is GenerationDiagnostic other && Equals(other); + public override int GetHashCode() => (Id, Message, Start, Length).GetHashCode(); +} diff --git a/source/Generation/MapperSourceOutput.cs b/source/Generation/MapperSourceOutput.cs index b111899..d88938a 100644 --- a/source/Generation/MapperSourceOutput.cs +++ b/source/Generation/MapperSourceOutput.cs @@ -1,80 +1,263 @@ +#nullable enable + using AlephMapper.Generation.Emitters; using AlephMapper.Helpers; using AlephMapper.Models; -using AlephMapper.Diagnostics; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; +using System.Threading; + +#if !DEBUG +using AlephMapper.Diagnostics; +#endif namespace AlephMapper.Generation; internal static class MapperSourceOutput { - public static void Generate(SourceProductionContext context, ImmutableArray mappings) + public static MapperGenerationResult Create( + GeneratorAttributeSyntaxContext attributeContext, + MapperAttributeKind attributeKind, + CancellationToken cancellationToken) { try { - if (mappings.Length == 0) + var mapperDeclaration = attributeContext.TargetNode switch { - return; + ClassDeclarationSyntax classDeclaration => classDeclaration, + MethodDeclarationSyntax { Parent: ClassDeclarationSyntax classDeclaration } => classDeclaration, + _ => null + }; + var mapperType = attributeContext.TargetSymbol switch + { + INamedTypeSymbol classSymbol => classSymbol, + IMethodSymbol methodSymbol => methodSymbol.ContainingType, + _ => null + }; + if (mapperDeclaration is null || mapperType is null) + { + return MapperGenerationResult.Empty; } - var mappingsByMethod = new Dictionary(SymbolHelpers.MethodComparer.Instance); - var mappingsByClass = new Dictionary>(SymbolEqualityComparer.Default); - foreach (var mapping in mappings) + var compilation = attributeContext.SemanticModel.Compilation; + var candidate = MapperCandidate.Create(attributeContext, attributeKind, cancellationToken); + if (!IsPrimaryMapperCandidate(compilation, mapperType, candidate, cancellationToken)) { - mappingsByMethod[SymbolHelpers.Normalize(mapping.MethodSymbol)] = mapping; - if (!mappingsByClass.TryGetValue(mapping.ContainingType, out var classMappings)) - { - classMappings = []; - mappingsByClass.Add(mapping.ContainingType, classMappings); - } + return MapperGenerationResult.Empty; + } - classMappings.Add(mapping); + var mappings = CreateMapperAnalyses(compilation, mapperType, cancellationToken); + if (mappings.Count == 0 || !mappings.Any(static mapping => + (mapping.IsProjectable || mapping.IsUpdatable || mapping.Adaptations.Count > 0) && mapping.IsClassPartial)) + { + return MapperGenerationResult.Empty; } - foreach (var pair in mappingsByClass) + var mappingsByMethod = new Dictionary(SymbolHelpers.MethodComparer.Instance); + foreach (var mapping in mappings) { - GenerateMapper(context, pair.Key, pair.Value, mappingsByMethod); + mappingsByMethod[SymbolHelpers.Normalize(mapping.MethodSymbol)] = mapping; } + + var catalog = new MappingCatalog( + mappingsByMethod, + method => CreateExternalAnalysis(compilation, method, cancellationToken)); + return GenerateMapper(mapperType, mappings, catalog); } - catch (System.Exception exception) - { - CrashDiagnosticsReporter.Report(context, exception); #if DEBUG + catch + { throw; + } +#else + catch (Exception exception) + { + return new MapperGenerationResult( + null, + null, + [CrashDiagnosticsReporter.CreateDiagnostic(exception)]); + } #endif + } + + public static void EmitSource( + SourceProductionContext context, + MapperSourceResult result) + { + if (result.HintName is not null && result.Source is not null) + { + context.AddSource(result.HintName, result.Source); + } + } + + public static void EmitDiagnostics( + SourceProductionContext context, + MapperGenerationResult result, + Compilation compilation) + { + foreach (var diagnostic in result.Diagnostics) + { + context.ReportDiagnostic(diagnostic.ToDiagnostic(compilation)); } } - private static void GenerateMapper( - SourceProductionContext sourceProductionContext, + private static IReadOnlyList CreateMapperAnalyses( + Compilation compilation, INamedTypeSymbol mapperType, - IReadOnlyList mappings, - IDictionary mappingsByMethod) + CancellationToken cancellationToken) { - if (!mappings.Any(static mapping => - (mapping.IsExpressive || mapping.IsUpdatable || mapping.Adaptations.Count > 0) && mapping.IsClassPartial)) + var mappings = new List(); + foreach (var declarationReference in mapperType.DeclaringSyntaxReferences + .OrderBy(static reference => reference.SyntaxTree.FilePath, StringComparer.Ordinal) + .ThenBy(static reference => reference.Span.Start)) { - return; + if (declarationReference.GetSyntax(cancellationToken) is not ClassDeclarationSyntax declaration) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree); + foreach (var method in declaration.Members.OfType() + .Where(static method => method.ExpressionBody is not null) + .OrderBy(static method => method.SpanStart)) + { + var mapping = MappingAnalysisFactory.Create(semanticModel, method, cancellationToken); + if (mapping is not null) + { + mappings.Add(mapping); + } + } } - var context = new MapperGenerationContext(mapperType, mappingsByMethod, sourceProductionContext); + return mappings + .OrderBy(static mapping => mapping.MethodSymbol.Locations.FirstOrDefault()?.SourceSpan.Start ?? int.MaxValue) + .ThenBy(static mapping => mapping.Name, StringComparer.Ordinal) + .ToArray(); + } + + private static MappingAnalysis? CreateExternalAnalysis( + Compilation compilation, + IMethodSymbol method, + CancellationToken cancellationToken) + { + foreach (var declarationReference in method.DeclaringSyntaxReferences + .OrderBy(static reference => reference.SyntaxTree.FilePath, StringComparer.Ordinal) + .ThenBy(static reference => reference.Span.Start)) + { + if (declarationReference.GetSyntax(cancellationToken) is not MethodDeclarationSyntax declaration || + declaration.ExpressionBody is null) + { + continue; + } + + var semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree); + return MappingAnalysisFactory.Create(semanticModel, declaration, cancellationToken); + } + + return null; + } + + private static bool IsPrimaryMapperCandidate( + Compilation compilation, + INamedTypeSymbol mapperType, + MapperCandidate candidate, + CancellationToken cancellationToken) + { + var primaryCandidate = mapperType.DeclaringSyntaxReferences + .OrderBy(static reference => reference.SyntaxTree.FilePath, StringComparer.Ordinal) + .ThenBy(static reference => reference.Span.Start) + .Select(reference => reference.GetSyntax(cancellationToken) as ClassDeclarationSyntax) + .Where(static declaration => declaration is not null) + .SelectMany(declaration => GetMapperCandidates(compilation, declaration!, cancellationToken)) + .OrderBy(static current => current.FilePath, StringComparer.Ordinal) + .ThenBy(static current => current.Start) + .ThenBy(static current => current.Length) + .ThenBy(static current => current.AttributeKind) + .FirstOrDefault(); + + return primaryCandidate is not null && primaryCandidate.Equals(candidate); + } + + private static IEnumerable GetMapperCandidates( + Compilation compilation, + ClassDeclarationSyntax mapperDeclaration, + CancellationToken cancellationToken) + { + var semanticModel = compilation.GetSemanticModel(mapperDeclaration.SyntaxTree); + + if (ContainsAlephMapperAttribute(mapperDeclaration.AttributeLists, semanticModel, cancellationToken, out var classKind)) + { + yield return CreateCandidate(mapperDeclaration, classKind); + } + + foreach (var method in mapperDeclaration.Members.OfType()) + { + if (ContainsAlephMapperAttribute(method.AttributeLists, semanticModel, cancellationToken, out var methodKind)) + { + yield return CreateCandidate(method, methodKind); + } + } + } + + private static bool ContainsAlephMapperAttribute( + SyntaxList attributeLists, + SemanticModel semanticModel, + CancellationToken cancellationToken, + out MapperAttributeKind attributeKind) + { + attributeKind = MapperAttributeKind.Adapt; + foreach (var attribute in attributeLists.SelectMany(static list => list.Attributes)) + { + var type = semanticModel.GetTypeInfo(attribute, cancellationToken).Type; + switch (type?.ToDisplayString()) + { + case "AlephMapper.ProjectableAttribute": + attributeKind = MapperAttributeKind.Projectable; + return true; + case "AlephMapper.UpdatableAttribute": + attributeKind = MapperAttributeKind.Updatable; + return true; + case "AlephMapper.AdaptAttribute": + attributeKind = MapperAttributeKind.Adapt; + return true; + } + } + + return false; + } + + private static MapperCandidate CreateCandidate(SyntaxNode targetNode, MapperAttributeKind attributeKind) + { + return new MapperCandidate( + targetNode.SyntaxTree.FilePath, + targetNode.SpanStart, + targetNode.Span.Length, + attributeKind); + } + + private static MapperGenerationResult GenerateMapper( + INamedTypeSymbol mapperType, + IReadOnlyList mappings, + MappingCatalog catalog) + { + var context = new MapperGenerationContext(mapperType, catalog); foreach (var mapping in mappings) { - if (!mapping.IsExpressive && !mapping.IsUpdatable && mapping.Adaptations.Count == 0) + if (!mapping.IsProjectable && !mapping.IsUpdatable && mapping.Adaptations.Count == 0) { continue; } var details = new MappingMethodDetails(mapping); - ExpressiveMemberEmitter.Emit(details, context); + ProjectableMemberEmitter.Emit(details, context); AdaptationMemberEmitter.Emit(details, context); UpdatableMemberEmitter.Emit(details, context); } var generatedFile = MapperFileEmitter.Render(context); - sourceProductionContext.AddSource(generatedFile.HintName, generatedFile.Source); + return new MapperGenerationResult(generatedFile.HintName, generatedFile.Source, context.GetDiagnostics()); } } diff --git a/source/Generation/MapperSourceResult.cs b/source/Generation/MapperSourceResult.cs new file mode 100644 index 0000000..14e1e13 --- /dev/null +++ b/source/Generation/MapperSourceResult.cs @@ -0,0 +1,22 @@ +#nullable enable + +using System; + +namespace AlephMapper.Generation; + +/// +/// The source-only portion of a mapper generation result. Keeping this value +/// separate ensures diagnostic changes do not invalidate source emission. +/// +internal readonly struct MapperSourceResult(string? hintName, string? source) : IEquatable +{ + public string? HintName { get; } = hintName; + public string? Source { get; } = source; + + public bool Equals(MapperSourceResult other) => + string.Equals(HintName, other.HintName, StringComparison.Ordinal) && + string.Equals(Source, other.Source, StringComparison.Ordinal); + + public override bool Equals(object? obj) => obj is MapperSourceResult other && Equals(other); + public override int GetHashCode() => (HintName, Source).GetHashCode(); +} diff --git a/source/Generation/MappingModelFactory.cs b/source/Generation/MappingAnalysisFactory.cs similarity index 79% rename from source/Generation/MappingModelFactory.cs rename to source/Generation/MappingAnalysisFactory.cs index d76659a..4e48b24 100644 --- a/source/Generation/MappingModelFactory.cs +++ b/source/Generation/MappingAnalysisFactory.cs @@ -11,18 +11,19 @@ namespace AlephMapper.Generation; -internal static class MappingModelFactory +internal static class MappingAnalysisFactory { - public static MappingModel? Create(GeneratorSyntaxContext context, CancellationToken cancellationToken) + public static MappingAnalysis? Create( + SemanticModel semanticModel, + MethodDeclarationSyntax methodDeclaration, + CancellationToken cancellationToken) { - if (context.Node is not MethodDeclarationSyntax methodDeclaration || - methodDeclaration.Parent is not ClassDeclarationSyntax classDeclaration || + if (methodDeclaration.Parent is not ClassDeclarationSyntax classDeclaration || !classDeclaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.StaticKeyword))) { return null; } - var semanticModel = context.SemanticModel; var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken); var methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken); if (classSymbol == null || methodSymbol == null || methodSymbol.Parameters.Length == 0) @@ -36,7 +37,7 @@ methodDeclaration.Parent is not ClassDeclarationSyntax classDeclaration || return null; } - return new MappingModel( + return new MappingAnalysis( classSymbol, methodSymbol, methodSymbol.Name, @@ -44,7 +45,7 @@ methodDeclaration.Parent is not ClassDeclarationSyntax classDeclaration || methodSymbol.ReturnType, bodyExpression, semanticModel, - HasAttribute(classSymbol, methodSymbol, typeof(ExpressiveAttribute).FullName), + HasAttribute(classSymbol, methodSymbol, typeof(ProjectableAttribute).FullName), HasAttribute(classSymbol, methodSymbol, typeof(UpdatableAttribute).FullName), classDeclaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)), GetNullStrategy(methodSymbol) ?? GetNullStrategy(classSymbol) ?? NullConditionalRewrite.Ignore, @@ -59,9 +60,9 @@ private static bool HasAttribute(INamedTypeSymbol classSymbol, IMethodSymbol met SymbolHelpers.HasAttribute(methodSymbol, attributeName); } - private static IReadOnlyList GetAdaptations(IMethodSymbol methodSymbol) + private static IReadOnlyList GetAdaptations(IMethodSymbol methodSymbol) { - var adaptations = new List(); + var adaptations = new List(); foreach (var attribute in methodSymbol.GetAttributes()) { if (attribute.AttributeClass?.ToDisplayString() != typeof(AdaptAttribute).FullName || @@ -91,7 +92,13 @@ attribute.ConstructorArguments[0].Value is not INamedTypeSymbol sourceType || } } - adaptations.Add(new AdaptationModel(sourceType, destinationType, name, generation, nullStrategy, attribute)); + adaptations.Add(new AdaptationAnalysis( + sourceType, + destinationType, + name, + generation, + nullStrategy, + SourceLocationModel.FromSyntax(attribute.ApplicationSyntaxReference))); } return adaptations; @@ -101,8 +108,8 @@ attribute.ConstructorArguments[0].Value is not INamedTypeSymbol sourceType || { var value = SymbolHelpers.GetAttributeArgumentValue( symbol, - typeof(ExpressiveAttribute).FullName, - nameof(ExpressiveAttribute.NullConditionalRewrite)); + typeof(ProjectableAttribute).FullName, + nameof(ProjectableAttribute.NullConditionalRewrite)); return value is int intValue ? (NullConditionalRewrite)intValue : null; } @@ -130,17 +137,25 @@ private static IReadOnlyList ExtractUsingDirectives(SyntaxNode node) var usings = new HashSet(); foreach (var usingDirective in compilationUnit.Usings) { - usings.Add(usingDirective.Name.ToString()); + AddUsing(usings, usingDirective); } foreach (var namespaceDeclaration in compilationUnit.DescendantNodes().OfType()) { foreach (var usingDirective in namespaceDeclaration.Usings) { - usings.Add(usingDirective.Name.ToString()); + AddUsing(usings, usingDirective); } } return usings.OrderBy(static value => value).ToList(); } + + private static void AddUsing(ISet usings, UsingDirectiveSyntax usingDirective) + { + if (usingDirective.Name is { } name) + { + usings.Add(name.ToString()); + } + } } diff --git a/source/Generation/MappingCatalog.cs b/source/Generation/MappingCatalog.cs new file mode 100644 index 0000000..d1982fa --- /dev/null +++ b/source/Generation/MappingCatalog.cs @@ -0,0 +1,39 @@ +#nullable enable + +using AlephMapper.Helpers; +using AlephMapper.Models; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; + +namespace AlephMapper.Generation; + +/// +/// Resolves local mappings eagerly and source-defined helper mappings lazily. +/// This keeps each mapper output independent while preserving cross-mapper +/// inlining support. +/// +internal sealed class MappingCatalog( + Dictionary mappings, + Func createExternalMapping) +{ + public bool TryGetValue(IMethodSymbol method, out MappingAnalysis mapping) + { + var normalizedMethod = SymbolHelpers.Normalize(method); + if (mappings.TryGetValue(normalizedMethod, out mapping)) + { + return true; + } + + var externalMapping = createExternalMapping(normalizedMethod); + if (externalMapping is null) + { + mapping = null!; + return false; + } + + mappings[normalizedMethod] = externalMapping; + mapping = externalMapping; + return true; + } +} diff --git a/source/Generation/MappingMethodCandidate.cs b/source/Generation/MappingMethodCandidate.cs index 870d1b4..144a2a6 100644 --- a/source/Generation/MappingMethodCandidate.cs +++ b/source/Generation/MappingMethodCandidate.cs @@ -1,5 +1,7 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; using System.Threading; namespace AlephMapper.Generation; @@ -8,6 +10,28 @@ internal static class MappingMethodCandidate { public static bool IsCandidate(SyntaxNode node, CancellationToken _) { - return node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax }; + if (node is not MethodDeclarationSyntax + { + Parent: ClassDeclarationSyntax containingClass, + ExpressionBody: not null + } method) + { + return false; + } + + if (!containingClass.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.StaticKeyword))) + { + return false; + } + + return method.AttributeLists.Count != 0 || + containingClass.AttributeLists.Count != 0 || + containingClass.Members.OfType().Any(static member => + member.AttributeLists.Count != 0) || + containingClass.Members.OfType().Any(static member => + member.ParameterList.Parameters.Any(static parameter => + parameter.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.ThisKeyword)))) || + method.ParameterList.Parameters.Any(static parameter => + parameter.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.ThisKeyword))); } } diff --git a/source/Generation/MappingMethodDetails.cs b/source/Generation/MappingMethodDetails.cs index 1e4fc48..d0e128d 100644 --- a/source/Generation/MappingMethodDetails.cs +++ b/source/Generation/MappingMethodDetails.cs @@ -1,5 +1,8 @@ using AlephMapper.Helpers; using AlephMapper.Models; +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; namespace AlephMapper.Generation; @@ -9,32 +12,37 @@ namespace AlephMapper.Generation; /// internal sealed class MappingMethodDetails { - public MappingMethodDetails(MappingModel mapping) + public MappingMethodDetails(MappingAnalysis mapping) { Mapping = mapping; var nullableContextPosition = mapping.MethodSymbol.Locations.FirstOrDefault()?.SourceSpan.Start ?? 0; - var nullableContext = mapping.SemanticModel.GetNullableContext(nullableContextPosition); + var nullablePolicy = NullablePolicy.From(mapping.SemanticModel, nullableContextPosition); ParameterTypeNames = mapping.Parameters - .Select(parameter => TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, nullableContext)) + .Select(parameter => TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, nullablePolicy)) .ToArray(); - DestinationTypeName = TypeDisplay.ForSymbol(mapping.ReturnType, mapping.MethodSymbol.ReturnNullableAnnotation, nullableContext); - SourceTypeName = TypeDisplay.ForSymbol(mapping.ParamType, mapping.Parameters[0].NullableAnnotation, nullableContext); + DestinationTypeName = TypeDisplay.ForSymbol(mapping.ReturnType, mapping.ReturnType.NullableAnnotation, nullablePolicy); + SourceTypeName = TypeDisplay.ForSymbol(mapping.ParamType, mapping.ParamType.NullableAnnotation, nullablePolicy); SourceName = mapping.Parameters[0].Name; MethodParameterList = string.Join(", ", ParameterTypeNames); MethodParameterListWithNames = string.Join(", ", mapping.Parameters.Select(parameter => - $"{TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, nullableContext)} {parameter.Name}")); + $"{TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, nullablePolicy)} {parameter.Name}")); ExtraExpressionParameterTypeNames = ParameterTypeNames.Skip(1).ToArray(); ExtraExpressionParameterListWithNames = string.Join(", ", mapping.Parameters.Skip(1).Select(parameter => - $"{TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, nullableContext)} {parameter.Name}")); + $"{TypeDisplay.ForSymbol(parameter.Type, parameter.NullableAnnotation, nullablePolicy)} {parameter.Name}")); LambdaParameters = mapping.Parameters.Count == 1 ? mapping.Parameters[0].Name : "(" + string.Join(", ", mapping.Parameters.Select(parameter => parameter.Name)) + ")"; ProjectionLambdaParameter = mapping.Parameters[0].Name; - NullableContext = nullableContext; + MethodTypeParameterList = mapping.MethodSymbol.TypeParameters.Length == 0 + ? string.Empty + : "<" + string.Join(", ", mapping.MethodSymbol.TypeParameters.Select(static parameter => parameter.Name)) + ">"; + MethodTypeParameterCount = mapping.MethodSymbol.TypeParameters.Length; + MethodConstraintClauses = BuildConstraintClauses(mapping.MethodSymbol.TypeParameters, nullablePolicy); + NullablePolicy = nullablePolicy; } - public MappingModel Mapping { get; } + public MappingAnalysis Mapping { get; } public string[] ParameterTypeNames { get; } public string DestinationTypeName { get; } public string SourceTypeName { get; } @@ -45,5 +53,52 @@ public MappingMethodDetails(MappingModel mapping) public string ExtraExpressionParameterListWithNames { get; } public string LambdaParameters { get; } public string ProjectionLambdaParameter { get; } - public Microsoft.CodeAnalysis.NullableContext NullableContext { get; } + public string MethodTypeParameterList { get; } + public int MethodTypeParameterCount { get; } + public IReadOnlyList MethodConstraintClauses { get; } + public NullablePolicy NullablePolicy { get; } + + private static IReadOnlyList BuildConstraintClauses( + ImmutableArray typeParameters, + NullablePolicy nullablePolicy) + { + var clauses = new List(); + foreach (var typeParameter in typeParameters) + { + var constraints = new List(); + if (typeParameter.HasUnmanagedTypeConstraint) + { + constraints.Add("unmanaged"); + } + else if (typeParameter.HasValueTypeConstraint) + { + constraints.Add("struct"); + } + else if (typeParameter.HasReferenceTypeConstraint) + { + constraints.Add(typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated + ? "class?" + : "class"); + } + else if (typeParameter.HasNotNullConstraint) + { + constraints.Add("notnull"); + } + + constraints.AddRange(typeParameter.ConstraintTypes.Select(type => + TypeDisplay.ForSymbol(type, type.NullableAnnotation, nullablePolicy))); + + if (typeParameter.HasConstructorConstraint && !typeParameter.HasValueTypeConstraint) + { + constraints.Add("new()"); + } + + if (constraints.Count > 0) + { + clauses.Add($"where {typeParameter.Name} : {string.Join(", ", constraints)}"); + } + } + + return clauses; + } } diff --git a/source/Generation/MethodSignature.cs b/source/Generation/MethodSignature.cs index 9a99ac0..135539f 100644 --- a/source/Generation/MethodSignature.cs +++ b/source/Generation/MethodSignature.cs @@ -6,9 +6,10 @@ namespace AlephMapper.Generation; internal static class MethodSignature { - public static string Build(string name, IEnumerable parameterTypeNames) + public static string Build(string name, IEnumerable parameterTypeNames, int typeParameterCount = 0) { - return name + "(" + string.Join(",", parameterTypeNames.Select(RemoveNullableMarker)) + ")"; + return name + (typeParameterCount == 0 ? string.Empty : "`" + typeParameterCount) + + "(" + string.Join(",", parameterTypeNames.Select(RemoveNullableMarker)) + ")"; } private static string RemoveNullableMarker(string typeName) diff --git a/source/Helpers/EmitHelpers.cs b/source/Helpers/EmitHelpers.cs index 14b0048..17239ff 100644 --- a/source/Helpers/EmitHelpers.cs +++ b/source/Helpers/EmitHelpers.cs @@ -2,7 +2,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; -using System.Linq; namespace AlephMapper.Helpers; @@ -15,6 +14,7 @@ public static bool TryBuildUpdateAssignmentsWithInlining( ITypeSymbol sourceType, IReadOnlyList sourceParameterNames, CollectionPropertiesPolicy collectionPolicy, + NullablePolicy nullablePolicy, List lines) { // Seed type collection with the destination (return) type to reliably resolve @@ -28,7 +28,7 @@ public static bool TryBuildUpdateAssignmentsWithInlining( var typeContext = propertyInfoCollector.TypeContext; - var processor = new UpdatableMethodGenerator(destPrefix, typeContext, sourceParameterNames); + var processor = new UpdatableMethodGenerator(destPrefix, typeContext, sourceParameterNames, nullablePolicy); List processedLines; var srcName = sourceParameterNames[0]; diff --git a/source/Helpers/NullablePolicy.cs b/source/Helpers/NullablePolicy.cs new file mode 100644 index 0000000..ce3bb78 --- /dev/null +++ b/source/Helpers/NullablePolicy.cs @@ -0,0 +1,55 @@ +using Microsoft.CodeAnalysis; + +namespace AlephMapper.Helpers; + +/// +/// The effective nullable policy for one generated mapping member. +/// +internal readonly struct NullablePolicy +{ + private NullablePolicy(NullableContext context) + { + Context = context; + } + + public NullableContext Context { get; } + + public bool AnnotationsEnabled => Context is + NullableContext.Enabled or NullableContext.AnnotationsEnabled; + + public bool WarningsEnabled => Context is + NullableContext.Enabled or NullableContext.WarningsEnabled; + + public string Directive => Context switch + { + NullableContext.Enabled => "enable", + NullableContext.WarningsEnabled => "enable warnings", + NullableContext.AnnotationsEnabled => "enable annotations", + _ => "disable" + }; + + public static NullablePolicy Disabled { get; } = new(NullableContext.Disabled); + + public static NullablePolicy From(SemanticModel model, int position) + { + var context = model.GetNullableContext(position); + var projectOptions = model.Compilation.Options.NullableContextOptions; + var projectWarningsEnabled = projectOptions is + NullableContextOptions.Enable or NullableContextOptions.Warnings; + var projectAnnotationsEnabled = projectOptions is + NullableContextOptions.Enable or NullableContextOptions.Annotations; + var warningsEnabled = (context & NullableContext.WarningsEnabled) != 0 || + ((context & NullableContext.WarningsContextInherited) != 0 && projectWarningsEnabled); + var annotationsEnabled = (context & NullableContext.AnnotationsEnabled) != 0 || + ((context & NullableContext.AnnotationsContextInherited) != 0 && projectAnnotationsEnabled); + var effectiveContext = (warningsEnabled, annotationsEnabled) switch + { + (true, true) => NullableContext.Enabled, + (true, false) => NullableContext.WarningsEnabled, + (false, true) => NullableContext.AnnotationsEnabled, + _ => NullableContext.Disabled + }; + + return new NullablePolicy(effectiveContext); + } +} diff --git a/source/Helpers/TypeDisplay.cs b/source/Helpers/TypeDisplay.cs index 018f142..aa9ad3a 100644 --- a/source/Helpers/TypeDisplay.cs +++ b/source/Helpers/TypeDisplay.cs @@ -9,33 +9,31 @@ namespace AlephMapper.Helpers; internal static class TypeDisplay { private static readonly SymbolDisplayFormat NullableFormat = - SymbolDisplayFormat.MinimallyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); private static readonly SymbolDisplayFormat NonNullableFormat = - SymbolDisplayFormat.MinimallyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); public static string ForSymbol(ITypeSymbol symbol, SemanticModel model, int position) - => ForSymbol(symbol, symbol.NullableAnnotation, model.GetNullableContext(position)); + => ForSymbol(symbol, symbol.NullableAnnotation, NullablePolicy.From(model, position)); - public static string ForSymbol(ITypeSymbol symbol, NullableAnnotation annotationOverride, NullableContext nullableContext) + public static string ForSymbol(ITypeSymbol symbol, NullableAnnotation annotationOverride, NullablePolicy nullablePolicy) { - var annotationsEnabled = nullableContext is NullableContext.Enabled - or NullableContext.AnnotationsEnabled - or NullableContext.AnnotationsContextInherited; - - var format = annotationsEnabled ? NullableFormat : NonNullableFormat; - var display = symbol.ToDisplayString(format); - + var format = nullablePolicy.AnnotationsEnabled ? NullableFormat : NonNullableFormat; var effectiveAnnotation = annotationOverride != NullableAnnotation.None ? annotationOverride : symbol.NullableAnnotation; + var displaySymbol = annotationOverride != NullableAnnotation.None + ? symbol.WithNullableAnnotation(annotationOverride) + : symbol; + var display = displaySymbol.ToDisplayString(format); - if (annotationsEnabled && + if (nullablePolicy.AnnotationsEnabled && effectiveAnnotation == NullableAnnotation.Annotated && !display.EndsWith("?", StringComparison.Ordinal)) { diff --git a/source/Models/AdaptationAnalysis.cs b/source/Models/AdaptationAnalysis.cs new file mode 100644 index 0000000..6984db1 --- /dev/null +++ b/source/Models/AdaptationAnalysis.cs @@ -0,0 +1,82 @@ +#nullable enable + +using Microsoft.CodeAnalysis; + +namespace AlephMapper.Models; + +/// +/// Compiler-bound adaptation analysis state used only while rendering output. +/// +internal sealed class AdaptationAnalysis( + INamedTypeSymbol sourceType, + INamedTypeSymbol destinationType, + string? generatedName, + AdaptGeneration generation, + NullConditionalRewrite nullStrategy, + SourceLocationModel? location) +{ + public INamedTypeSymbol SourceType { get; } = sourceType; + public INamedTypeSymbol DestinationType { get; } = destinationType; + public string? GeneratedName { get; } = generatedName; + public AdaptGeneration Generation { get; } = generation; + public NullConditionalRewrite NullStrategy { get; } = nullStrategy; + public SourceLocationModel? Location { get; } = location; +} + +internal sealed class SourceLocationModel +{ + private SourceLocationModel( + string? filePath, + int start, + int length, + int startLine, + int startCharacter, + int endLine, + int endCharacter) + { + FilePath = filePath; + Start = start; + Length = length; + StartLine = startLine; + StartCharacter = startCharacter; + EndLine = endLine; + EndCharacter = endCharacter; + } + + public string? FilePath { get; } + public int Start { get; } + public int Length { get; } + public int StartLine { get; } + public int StartCharacter { get; } + public int EndLine { get; } + public int EndCharacter { get; } + + public static SourceLocationModel? FromSyntax(SyntaxReference? syntaxReference) + { + if (syntaxReference is null) + { + return null; + } + + var syntax = syntaxReference.GetSyntax(); + var location = syntax.GetLocation(); + var lineSpan = location.GetLineSpan(); + return new SourceLocationModel( + lineSpan.Path, + location.SourceSpan.Start, + location.SourceSpan.Length, + lineSpan.StartLinePosition.Line, + lineSpan.StartLinePosition.Character, + lineSpan.EndLinePosition.Line, + lineSpan.EndLinePosition.Character); + } + + public Location ToLocation() + { + var span = new Microsoft.CodeAnalysis.Text.TextSpan(Start, Length); + var lineSpan = new Microsoft.CodeAnalysis.Text.LinePositionSpan( + new Microsoft.CodeAnalysis.Text.LinePosition(StartLine, StartCharacter), + new Microsoft.CodeAnalysis.Text.LinePosition(EndLine, EndCharacter)); + return Microsoft.CodeAnalysis.Location.Create(FilePath ?? string.Empty, span, lineSpan); + } +} diff --git a/source/Models/AdaptationModel.cs b/source/Models/AdaptationModel.cs deleted file mode 100644 index 49e8aba..0000000 --- a/source/Models/AdaptationModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -#nullable enable - -using Microsoft.CodeAnalysis; - -namespace AlephMapper.Models; - -internal sealed class AdaptationModel -{ - public AdaptationModel( - INamedTypeSymbol sourceType, - INamedTypeSymbol destinationType, - string? generatedName, - AdaptGeneration generation, - NullConditionalRewrite nullStrategy, - AttributeData attribute) - { - SourceType = sourceType; - DestinationType = destinationType; - GeneratedName = generatedName; - Generation = generation; - NullStrategy = nullStrategy; - Attribute = attribute; - } - - public INamedTypeSymbol SourceType { get; } - public INamedTypeSymbol DestinationType { get; } - public string? GeneratedName { get; } - public AdaptGeneration Generation { get; } - public NullConditionalRewrite NullStrategy { get; } - public AttributeData Attribute { get; } -} diff --git a/source/Models/MappingModel.cs b/source/Models/MappingAnalysis.cs similarity index 69% rename from source/Models/MappingModel.cs rename to source/Models/MappingAnalysis.cs index 7262d88..ea1bda7 100644 --- a/source/Models/MappingModel.cs +++ b/source/Models/MappingAnalysis.cs @@ -1,10 +1,14 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; namespace AlephMapper.Models; -internal sealed class MappingModel( +/// +/// Compiler-bound analysis state used only within a source-output callback. +/// It is intentionally not retained by the incremental pipeline. +/// +internal sealed class MappingAnalysis( INamedTypeSymbol containingType, IMethodSymbol methodSymbol, string name, @@ -12,13 +16,13 @@ internal sealed class MappingModel( ITypeSymbol returnType, ArrowExpressionClauseSyntax bodySyntax, SemanticModel semanticModel, - bool isExpressive, + bool isProjectable, bool isUpdatable, bool classIsStaticAndPartial, NullConditionalRewrite nullStrategy, CollectionPropertiesPolicy collectionPolicy, IReadOnlyList usingDirectives, - IReadOnlyList adaptations) + IReadOnlyList adaptations) { public readonly INamedTypeSymbol ContainingType = containingType; public readonly IMethodSymbol MethodSymbol = methodSymbol; @@ -29,26 +33,12 @@ internal sealed class MappingModel( public readonly ArrowExpressionClauseSyntax BodySyntax = bodySyntax; public readonly SemanticModel SemanticModel = semanticModel; - public readonly bool IsExpressive = isExpressive; + public readonly bool IsProjectable = isProjectable; public readonly bool IsUpdatable = isUpdatable; public readonly bool IsClassPartial = classIsStaticAndPartial; public readonly NullConditionalRewrite NullStrategy = nullStrategy; public readonly CollectionPropertiesPolicy CollectionPolicy = collectionPolicy; public readonly IReadOnlyList UsingDirectives = usingDirectives; - public readonly IReadOnlyList Adaptations = adaptations; - - public override bool Equals(object obj) - { - if (obj is MappingModel other) - { - return SymbolEqualityComparer.Default.Equals(MethodSymbol, other.MethodSymbol); - } - return false; - } - - public override int GetHashCode() - { - return SymbolEqualityComparer.Default.GetHashCode(MethodSymbol); - } + public readonly IReadOnlyList Adaptations = adaptations; } diff --git a/source/PrettyPrinter.cs b/source/PrettyPrinter.cs index 0f5f549..f083e83 100644 --- a/source/PrettyPrinter.cs +++ b/source/PrettyPrinter.cs @@ -118,11 +118,21 @@ public override void DefaultVisit(SyntaxNode node) public override void VisitConditionalExpression(ConditionalExpressionSyntax node) { + if (!HasMultilineConditionalLayout(node)) + { + Visit(node.Condition.WithoutTrailingTrivia()); + WriteRaw(" ? "); + Visit(node.WhenTrue.WithoutLeadingTrivia().WithoutTrailingTrivia()); + WriteRaw(" : "); + Visit(node.WhenFalse.WithoutLeadingTrivia()); + return; + } + Visit(node.Condition.WithoutTrailingTrivia()); WriteLine(); Indent(); WriteRaw("? "); - Visit(node.WhenTrue.WithoutLeadingTrivia()); + Visit(node.WhenTrue.WithoutLeadingTrivia().WithoutTrailingTrivia()); WriteLine(); WriteRaw(": "); Visit(node.WhenFalse.WithoutLeadingTrivia()); @@ -354,6 +364,24 @@ private static bool HasLineBreakInLogicalChain(ExpressionSyntax expression, Synt HasLineBreakInLogicalChain(binaryExpression.Right, chainKind); } + private static bool HasMultilineConditionalLayout(ConditionalExpressionSyntax node) + { + if (node.GetAnnotations(GeneratedSyntaxAnnotations.MultilineConditional).Any()) + { + return true; + } + + if (node.GetAnnotations(GeneratedSyntaxAnnotations.SingleLineConditional).Any()) + { + return ContainsLineBreak(node.WhenTrue.ToFullString()) || + ContainsLineBreak(node.WhenFalse.ToFullString()); + } + + // Rewriters synthesize conditionals for null-conditional access. Keep their + // established, readable multi-line form unless the source layout was recorded. + return true; + } + private static bool ContainsLineBreak(SyntaxTriviaList trivia) { return trivia.Any(triviaItem => triviaItem.IsKind(SyntaxKind.EndOfLineTrivia)); diff --git a/source/PropertyTypeInfoCollector.cs b/source/PropertyTypeInfoCollector.cs index f3c8068..aa1d0ad 100644 --- a/source/PropertyTypeInfoCollector.cs +++ b/source/PropertyTypeInfoCollector.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; +#nullable enable + namespace AlephMapper; /// @@ -114,12 +116,13 @@ public override void VisitConditionalExpression(ConditionalExpressionSyntax node { try { - if (_currentTargetType != null) + if (_currentTargetType is not { } currentTargetType) { - _visitedTypes.Add(_currentTargetType); + return; } - - var trueCollector = new PropertyTypeInfoCollector(_currentTargetType, _rootPath) { TypeContext = TypeContext, _visitedTypes = _visitedTypes }; + + _visitedTypes.Add(currentTargetType); + var trueCollector = new PropertyTypeInfoCollector(currentTargetType, _rootPath) { TypeContext = TypeContext, _visitedTypes = _visitedTypes }; trueCollector.Visit(node.WhenTrue); trueCollector.Visit(node.WhenFalse); } @@ -129,3 +132,5 @@ public override void VisitConditionalExpression(ConditionalExpressionSyntax node } } } + +#nullable restore diff --git a/source/SyntaxRewriters/InliningResolver.CollectionExpressionRewriter.cs b/source/SyntaxRewriters/InliningResolver.CollectionExpressionRewriter.cs index 61962ae..e8e08e0 100644 --- a/source/SyntaxRewriters/InliningResolver.CollectionExpressionRewriter.cs +++ b/source/SyntaxRewriters/InliningResolver.CollectionExpressionRewriter.cs @@ -196,7 +196,7 @@ private static bool IsDescendantOf(SyntaxNode child, SyntaxNode potentialParent) return false; } - private static ExpressionSyntax CreateConstructorExpression(ITypeSymbol? targetType, SyntaxNode originalExpression) + private ExpressionSyntax CreateConstructorExpression(ITypeSymbol? targetType, SyntaxNode originalExpression) { if (targetType is INamedTypeSymbol namedType) { @@ -235,58 +235,36 @@ private static ExpressionSyntax CreateConstructorExpression(ITypeSymbol? targetT } // Ultimate fallback - create a generic empty list with string type for common scenarios - return ObjectCreationExpression( - GenericName(Identifier("List")) - .WithTypeArgumentList( - TypeArgumentList( - SingletonSeparatedList( - PredefinedType(Token(SyntaxKind.StringKeyword)) - ) - ) - ).WithLeadingTrivia(Space) - ).WithArgumentList(ArgumentList()) - .WithLeadingTrivia(originalExpression.GetLeadingTrivia()) - .WithTrailingTrivia(originalExpression.GetTrailingTrivia()); + return ParseExpression("new global::System.Collections.Generic.List()") + .WithLeadingTrivia(originalExpression.GetLeadingTrivia()) + .WithTrailingTrivia(originalExpression.GetTrailingTrivia()); } - private static ExpressionSyntax CreateListConstructor(ITypeSymbol elementType) + private ExpressionSyntax CreateListConstructor(ITypeSymbol elementType) { - var elementTypeSyntax = IdentifierName(elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - - return ObjectCreationExpression( - GenericName(Identifier("List")) - .WithTypeArgumentList( - TypeArgumentList( - SingletonSeparatedList(elementTypeSyntax) - ) - ) - .WithLeadingTrivia(Space) - ).WithArgumentList(ArgumentList()) - .WithNewKeyword(Token(SyntaxKind.NewKeyword).WithTrailingTrivia(Space)); + var elementTypeName = TypeDisplay.ForSymbol( + elementType, + elementType.NullableAnnotation, + nullablePolicy); + return ParseExpression($"new global::System.Collections.Generic.List<{elementTypeName}>()"); } - private static ExpressionSyntax CreateArrayExpression(ITypeSymbol elementType) + private ExpressionSyntax CreateArrayExpression(ITypeSymbol elementType) { // Create Array.Empty() for better performance - var elementTypeSyntax = IdentifierName(elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - - return InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - IdentifierName("Array"), - GenericName(Identifier("Empty")) - .WithTypeArgumentList( - TypeArgumentList( - SingletonSeparatedList(elementTypeSyntax) - ) - ) - ) - ).WithArgumentList(ArgumentList()); + var elementTypeName = TypeDisplay.ForSymbol( + elementType, + elementType.NullableAnnotation, + nullablePolicy); + return ParseExpression($"global::System.Array.Empty<{elementTypeName}>()"); } - private static ExpressionSyntax CreateGenericConstructor(INamedTypeSymbol namedType) + private ExpressionSyntax CreateGenericConstructor(INamedTypeSymbol namedType) { - var typeSyntax = IdentifierName(namedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + var typeSyntax = ParseTypeName(TypeDisplay.ForSymbol( + namedType, + NullableAnnotation.NotAnnotated, + nullablePolicy)); return ObjectCreationExpression(typeSyntax.WithLeadingTrivia(Space)) .WithArgumentList(ArgumentList()); diff --git a/source/SyntaxRewriters/InliningResolver.ConditionalExpressionRewriter.cs b/source/SyntaxRewriters/InliningResolver.ConditionalExpressionRewriter.cs new file mode 100644 index 0000000..abc6283 --- /dev/null +++ b/source/SyntaxRewriters/InliningResolver.ConditionalExpressionRewriter.cs @@ -0,0 +1,31 @@ +#nullable enable + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AlephMapper.SyntaxRewriters; + +internal sealed partial class InliningResolver +{ + public override SyntaxNode? VisitConditionalExpression(ConditionalExpressionSyntax node) + { + var rewritten = (ConditionalExpressionSyntax?)base.VisitConditionalExpression(node); + if (rewritten == null) + { + return null; + } + + var annotationKind = HasMultilineConditionalLayout(node) + ? GeneratedSyntaxAnnotations.MultilineConditional + : GeneratedSyntaxAnnotations.SingleLineConditional; + return rewritten.WithAdditionalAnnotations(new SyntaxAnnotation(annotationKind)); + } + + private static bool HasMultilineConditionalLayout(ConditionalExpressionSyntax node) + { + var text = node.ToFullString(); + return text.IndexOf('\n') >= 0 || text.IndexOf('\r') >= 0; + } +} + +#nullable restore diff --git a/source/SyntaxRewriters/InliningResolver.ImplicitObjectCreationRewriter.cs b/source/SyntaxRewriters/InliningResolver.ImplicitObjectCreationRewriter.cs index 67d3013..2240d2a 100644 --- a/source/SyntaxRewriters/InliningResolver.ImplicitObjectCreationRewriter.cs +++ b/source/SyntaxRewriters/InliningResolver.ImplicitObjectCreationRewriter.cs @@ -9,7 +9,16 @@ internal sealed partial class InliningResolver { public override SyntaxNode VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) { - var rewritten = base.VisitObjectCreationExpression(node)!; + var rewritten = (ObjectCreationExpressionSyntax)base.VisitObjectCreationExpression(node)!; + if (model.GetTypeInfo(node).Type is { } typeSymbol) + { + var typeName = AlephMapper.Helpers.TypeDisplay.ForSymbol( + typeSymbol, + NullableAnnotation.NotAnnotated, + nullablePolicy); + rewritten = rewritten.WithType(ParseTypeName(typeName).WithTriviaFrom(rewritten.Type)); + } + return IsAnnotatedReturnCreation(node) ? rewritten.WithAdditionalAnnotations(new SyntaxAnnotation(InlinedReturnCreationAnnotation)) : rewritten; @@ -17,14 +26,20 @@ public override SyntaxNode VisitObjectCreationExpression(ObjectCreationExpressio public override SyntaxNode VisitImplicitObjectCreationExpression(ImplicitObjectCreationExpressionSyntax implicitNew) { - var type = model.GetTypeInfo(implicitNew).Type?.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + var typeSymbol = model.GetTypeInfo(implicitNew).Type; + var type = typeSymbol == null + ? null + : AlephMapper.Helpers.TypeDisplay.ForSymbol( + typeSymbol, + NullableAnnotation.NotAnnotated, + nullablePolicy); if (type == null) { return base.VisitImplicitObjectCreationExpression(implicitNew); } - var objectCreation = ObjectCreationExpression(IdentifierName(type).WithTrailingTrivia(ElasticCarriageReturn)); + var objectCreation = ObjectCreationExpression(ParseTypeName(type).WithTrailingTrivia(ElasticCarriageReturn)); if (implicitNew.Initializer != null) { diff --git a/source/SyntaxRewriters/InliningResolver.InvocationRewriter.cs b/source/SyntaxRewriters/InliningResolver.InvocationRewriter.cs index 8aa2240..253b499 100644 --- a/source/SyntaxRewriters/InliningResolver.InvocationRewriter.cs +++ b/source/SyntaxRewriters/InliningResolver.InvocationRewriter.cs @@ -1,4 +1,7 @@ +#nullable enable + using AlephMapper.Helpers; +using AlephMapper.Generation; using AlephMapper.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -12,10 +15,11 @@ namespace AlephMapper.SyntaxRewriters; internal sealed partial class InliningResolver( SemanticModel model, - IDictionary catalog, + MappingCatalog catalog, bool forUpdateMethod, NullConditionalRewrite rewriteSupport, - ITypeSymbol returnTypeToAnnotate = null) + NullablePolicy nullablePolicy, + ITypeSymbol? returnTypeToAnnotate = null) : CSharpSyntaxRewriter { internal const string InlinedConditionalAnnotation = "AlephMapper.InlinedConditional"; @@ -23,7 +27,7 @@ internal sealed partial class InliningResolver( private HashSet _callStack = new(SymbolEqualityComparer.Default); private List _circularReferences = []; - private Dictionary _inlinedMethods = new(SymbolEqualityComparer.Default); + private Dictionary _inlinedMethods = new(SymbolEqualityComparer.Default); private List _unsafeConditionalReceivers = []; private List _unsupportedNullConditionals = []; public IEnumerable UsingDirectives => _inlinedMethods.SelectMany(il => il.Value.UsingDirectives).Distinct(); @@ -32,14 +36,14 @@ internal sealed partial class InliningResolver( public IReadOnlyList UnsafeConditionalReceivers => _unsafeConditionalReceivers; public IReadOnlyList UnsupportedNullConditionals => _unsupportedNullConditionals; - private IMethodSymbol ResolveMethodGroupSymbol(ExpressionSyntax expr) + private IMethodSymbol? ResolveMethodGroupSymbol(ExpressionSyntax expr) { var si = model.GetSymbolInfo(expr); if (si.Symbol is IMethodSymbol ms) return ms; return null; } - private static IMethodSymbol TryGetDelegateInvoke(IMethodSymbol invokedMethod, int argIndex) + private static IMethodSymbol? TryGetDelegateInvoke(IMethodSymbol invokedMethod, int argIndex) { if (argIndex < 0 || argIndex >= invokedMethod.Parameters.Length) return null; var p = invokedMethod.Parameters[argIndex].Type as INamedTypeSymbol; @@ -57,7 +61,7 @@ private void RecordCircularReference(IMethodSymbol method) _circularReferences.Add(circularRef); } - public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node) { if (node.Parent == null || model.GetSymbolInfo(node.Expression).Symbol is not IMethodSymbol invokedMethod) { @@ -101,14 +105,14 @@ public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax { _inlinedMethods[normalizedMethod] = callee; var inlinedBody = - (ExpressionSyntax)new InliningResolver(callee.SemanticModel, catalog, forUpdateMethod, rewriteSupport, callee.ReturnType) + (ExpressionSyntax)new InliningResolver(callee.SemanticModel, catalog, forUpdateMethod, rewriteSupport, nullablePolicy, callee.ReturnType) { _callStack = _callStack, _circularReferences = _circularReferences, _inlinedMethods = _inlinedMethods, _unsafeConditionalReceivers = _unsafeConditionalReceivers, _unsupportedNullConditionals = _unsupportedNullConditionals - }.Visit(callee.BodySyntax.Expression); + }.Visit(callee.BodySyntax.Expression)!; var substitutions = callee.Parameters.ToDictionary( p => p.Name, @@ -176,17 +180,17 @@ public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax try { _inlinedMethods[directCallMethod] = callee2; - var inlinedBody2 = (ExpressionSyntax)new InliningResolver(callee2.SemanticModel, catalog, forUpdateMethod, rewriteSupport, callee2.ReturnType) + var inlinedBody2 = (ExpressionSyntax)new InliningResolver(callee2.SemanticModel, catalog, forUpdateMethod, rewriteSupport, nullablePolicy, callee2.ReturnType) { _callStack = _callStack, _circularReferences = _circularReferences, _inlinedMethods = _inlinedMethods, _unsafeConditionalReceivers = _unsafeConditionalReceivers, _unsupportedNullConditionals = _unsupportedNullConditionals - }.Visit(callee2.BodySyntax.Expression); + }.Visit(callee2.BodySyntax.Expression)!; - var substituted = new ParameterSubstitutionRewriter(substitutionsMap) - .Visit(inlinedBody2) + var substituted = ((ExpressionSyntax)new ParameterSubstitutionRewriter(substitutionsMap) + .Visit(inlinedBody2)!) .WithoutTrivia(); if (conditionalAccessExpression) @@ -218,7 +222,7 @@ private bool TryBuildParameterSubstitutions( // We need the original definition's parameters (which include 'this') for correct substitution. var isReducedExtension = invokedMethod.IsExtensionMethod && invokedMethod.ReducedFrom != null; var parameters = isReducedExtension - ? invokedMethod.ReducedFrom.Parameters + ? invokedMethod.ReducedFrom!.Parameters : invokedMethod.Parameters; if (parameters.Length == 0) @@ -236,18 +240,18 @@ private bool TryBuildParameterSubstitutions( conditionalAccessExpression = true; if (node.Expression is MemberAccessExpressionSyntax memberAccess) { - receiver = (ExpressionSyntax?)(Visit(memberAccess.Expression) ?? memberAccess.Expression); + receiver = (ExpressionSyntax)(Visit(memberAccess.Expression) ?? memberAccess.Expression); } else { receiver = rewriteSupport != NullConditionalRewrite.None - ? _conditionalAccessExpressionsStack.Peek() - : (ExpressionSyntax?)(Visit(caExpr.Expression) ?? caExpr.Expression); + ? _conditionalAccessExpressionsStack.Peek()! + : (ExpressionSyntax)(Visit(caExpr.Expression) ?? caExpr.Expression); } } else if (node.Expression is MemberAccessExpressionSyntax memberAccess) { - receiver = (ExpressionSyntax?)(Visit(memberAccess.Expression) ?? memberAccess.Expression); + receiver = (ExpressionSyntax)(Visit(memberAccess.Expression) ?? memberAccess.Expression); } else { @@ -260,14 +264,10 @@ private bool TryBuildParameterSubstitutions( foreach (var arg in args) { - IParameterSymbol targetParam; + IParameterSymbol? targetParam; if (arg.NameColon != null) { targetParam = parameters.FirstOrDefault(p => p.Name == arg.NameColon.Name.Identifier.Text); - if (targetParam == null) - { - return false; - } } else { @@ -279,7 +279,12 @@ private bool TryBuildParameterSubstitutions( targetParam = parameters[nextParamIndex++]; } - var rewrittenArg = (ExpressionSyntax?)(Visit(arg.Expression) ?? arg.Expression); + if (targetParam == null) + { + return false; + } + + var rewrittenArg = (ExpressionSyntax)(Visit(arg.Expression) ?? arg.Expression); substitutions[targetParam.Name] = rewrittenArg; } @@ -302,26 +307,16 @@ internal class CircularReferenceInfo(IMethodSymbol method, IEnumerable ", callStack.Select(m => $"{m.ContainingType.Name}.{m.Name}")); } -internal sealed class UnsafeConditionalReceiverInfo +internal sealed class UnsafeConditionalReceiverInfo(ExpressionSyntax expression) { - public UnsafeConditionalReceiverInfo(ExpressionSyntax expression) - { - Expression = expression; - Location = expression.GetLocation(); - } - - public ExpressionSyntax Expression { get; } - public Location Location { get; } + public ExpressionSyntax Expression { get; } = expression; + public Location Location { get; } = expression.GetLocation(); } -internal sealed class UnsupportedNullConditionalInfo +internal sealed class UnsupportedNullConditionalInfo(ConditionalAccessExpressionSyntax expression) { - public UnsupportedNullConditionalInfo(ConditionalAccessExpressionSyntax expression) - { - Expression = expression; - Location = expression.GetLocation(); - } - - public ConditionalAccessExpressionSyntax Expression { get; } - public Location Location { get; } + public ConditionalAccessExpressionSyntax Expression { get; } = expression; + public Location Location { get; } = expression.GetLocation(); } + +#nullable restore diff --git a/source/SyntaxRewriters/InliningResolver.NullConditionalRewriter.cs b/source/SyntaxRewriters/InliningResolver.NullConditionalRewriter.cs index a271751..9a4798d 100644 --- a/source/SyntaxRewriters/InliningResolver.NullConditionalRewriter.cs +++ b/source/SyntaxRewriters/InliningResolver.NullConditionalRewriter.cs @@ -49,18 +49,52 @@ internal partial class InliningResolver if (rewriteSupport is NullConditionalRewrite.Ignore) { // Ignore the conditional access and simply return the accessed expression - return rewrittenWhenNotNull; + // Preserve the source semantics while suppressing the nullable-flow warning + // introduced by replacing `?.` with a regular member access. + if (!nullablePolicy.WarningsEnabled) + { + return rewrittenWhenNotNull; + } + + return rewrittenWhenNotNull is PostfixUnaryExpressionSyntax postfix && + postfix.IsKind(SyntaxKind.SuppressNullableWarningExpression) + ? rewrittenWhenNotNull + : PostfixUnaryExpression( + SyntaxKind.SuppressNullableWarningExpression, + rewrittenWhenNotNull); } if (rewriteSupport is NullConditionalRewrite.Rewrite) { var typeInfo = model.GetTypeInfo(node); var convertedType = typeInfo.ConvertedType ?? typeInfo.Type; - var nullableContext = model.GetNullableContext(node.SpanStart); if (convertedType is not null) { - var castTypeName = TypeDisplay.ForSymbol(convertedType, convertedType.NullableAnnotation, nullableContext); + // A null-conditional access always produces null when its receiver is null. + // Its converted type can still be non-nullable when it is immediately consumed + // by an expression such as `??`, so explicitly retain that nullability here. + var castTypeName = TypeDisplay.ForSymbol( + convertedType, + NullableAnnotation.Annotated, + nullablePolicy); + + var nullValue = convertedType.IsReferenceType && + convertedType.NullableAnnotation != NullableAnnotation.Annotated && + nullablePolicy.WarningsEnabled + ? (ExpressionSyntax)PostfixUnaryExpression( + SyntaxKind.SuppressNullableWarningExpression, + LiteralExpression(SyntaxKind.NullLiteralExpression)) + : LiteralExpression(SyntaxKind.NullLiteralExpression); + var nullBranch = CastExpression( + ParseTypeName(convertedType.IsReferenceType && + convertedType.NullableAnnotation != NullableAnnotation.Annotated + ? TypeDisplay.ForSymbol( + convertedType, + NullableAnnotation.NotAnnotated, + nullablePolicy) + : castTypeName), + nullValue); return ParenthesizedExpression( ConditionalExpression( @@ -72,10 +106,7 @@ internal partial class InliningResolver ParenthesizedExpression(rewrittenWhenNotNull.WithoutTrivia()) .WithLeadingTrivia(Space) .WithTrailingTrivia(Space), - CastExpression( - ParseTypeName(castTypeName), - LiteralExpression(SyntaxKind.NullLiteralExpression) - ).WithLeadingTrivia(Space) + nullBranch.WithLeadingTrivia(Space) ) ); } diff --git a/source/SyntaxRewriters/InliningResolver.SwitchExpressionRewriter.cs b/source/SyntaxRewriters/InliningResolver.SwitchExpressionRewriter.cs index 2a64636..8b4c45f 100644 --- a/source/SyntaxRewriters/InliningResolver.SwitchExpressionRewriter.cs +++ b/source/SyntaxRewriters/InliningResolver.SwitchExpressionRewriter.cs @@ -20,6 +20,11 @@ internal sealed partial class InliningResolver return switchExpression?.WithSwitchKeyword(node.SwitchKeyword.WithLeadingTrivia(Space)); } + // Preserve the switch layout after lowering it to conditionals. The generated + // nodes have no trivia of their own, so the pretty printer needs this signal. + var switchText = node.ToFullString(); + var multiline = switchText.IndexOf('\n') >= 0 || switchText.IndexOf('\r') >= 0; + // Reverse arms order to start from the default value var arms = node.Arms.Reverse(); @@ -61,10 +66,11 @@ internal sealed partial class InliningResolver ); } - currentExpression = ConditionalExpression( + currentExpression = CreateConditional( expression, armExpression, - currentExpression + currentExpression, + multiline ); continue; @@ -99,10 +105,11 @@ internal sealed partial class InliningResolver armExpression, declaration, governingExpressionNode); - currentExpression = ConditionalExpression( + currentExpression = CreateConditional( condition, modifiedArmExpression, - currentExpression + currentExpression, + multiline ); continue; @@ -148,6 +155,19 @@ private ExpressionSyntax VisitClean(ExpressionSyntax expression) return (rewritten ?? stripped).WithoutTrivia(); } + private static ConditionalExpressionSyntax CreateConditional( + ExpressionSyntax condition, + ExpressionSyntax whenTrue, + ExpressionSyntax whenFalse, + bool multiline) + { + var conditional = ConditionalExpression(condition, whenTrue, whenFalse); + var annotationKind = multiline + ? GeneratedSyntaxAnnotations.MultilineConditional + : GeneratedSyntaxAnnotations.SingleLineConditional; + return conditional.WithAdditionalAnnotations(new SyntaxAnnotation(annotationKind)); + } + private static BinaryExpressionSyntax PadBinaryOperator(BinaryExpressionSyntax expression) { var token = expression.OperatorToken; diff --git a/source/VersionInfo.cs b/source/VersionInfo.cs index 200252d..b9628fb 100644 --- a/source/VersionInfo.cs +++ b/source/VersionInfo.cs @@ -2,5 +2,12 @@ internal static class VersionInfo { - public static string Version => "0.6.2"; -} \ No newline at end of file + public static string Version { get; } = + System.Reflection.CustomAttributeExtensions + .GetCustomAttribute(typeof(VersionInfo).Assembly) + ?.InformationalVersion + ?.Split('+')[0] + is { } informationalVersion + ? $"{informationalVersion}.{typeof(VersionInfo).Assembly.GetName().Version?.Revision ?? 0}" + : typeof(VersionInfo).Assembly.GetName().Version?.ToString() ?? "unknown"; +} diff --git a/tests/AlephMapper.IntegrationTests/ArticleMappingMappers.cs b/tests/AlephMapper.IntegrationTests/ArticleMappingMappers.cs index a80f5de..b91cf0b 100644 --- a/tests/AlephMapper.IntegrationTests/ArticleMappingMappers.cs +++ b/tests/AlephMapper.IntegrationTests/ArticleMappingMappers.cs @@ -2,7 +2,7 @@ namespace AlephMapper.IntegrationTests; public static partial class ArticleOrderMapper { - [Expressive] + [Projectable] public static ArticleOrderDto Map(ArticleOrder order) => new( order.Id, diff --git a/tests/AlephMapper.IntegrationTests/Models.cs b/tests/AlephMapper.IntegrationTests/Models.cs index b03d9b4..139a6e4 100644 --- a/tests/AlephMapper.IntegrationTests/Models.cs +++ b/tests/AlephMapper.IntegrationTests/Models.cs @@ -75,7 +75,7 @@ public class Department public int Id { get; set; } [Required] - public string Name { get; set; } = ""; + public string? Name { get; set; } = ""; public string? Description { get; set; } diff --git a/tests/AlephMapper.IntegrationTests/MultiParamMappers.cs b/tests/AlephMapper.IntegrationTests/MultiParamMappers.cs index 5aca830..482cb7a 100644 --- a/tests/AlephMapper.IntegrationTests/MultiParamMappers.cs +++ b/tests/AlephMapper.IntegrationTests/MultiParamMappers.cs @@ -1,9 +1,9 @@ -namespace AlephMapper.IntegrationTests; +namespace AlephMapper.IntegrationTests; // ────────────────────────────────────────────────────────────────── -// 1. Expressive mapper with multi-parameter helper inlining +// 1. Projectable mapper with multi-parameter helper inlining // ────────────────────────────────────────────────────────────────── -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class MultiParamEmployeeMapper { // Two-parameter helper: concatenate first + last @@ -27,7 +27,7 @@ public static string GetDepartmentName(Employee employee) => employee.Department?.Name ?? "Unassigned"; // ── Expression mapping that uses all the above helpers ── - [Expressive] + [Projectable] public static EmployeeDto MapToDto(Employee e) => new() { Id = e.Id, @@ -38,7 +38,7 @@ public static string GetDepartmentName(Employee employee) => }; // Mapping that exercises three-parameter helper - [Expressive] + [Projectable] public static EmployeeSimpleDto MapToSimpleDto(Employee employee) => new() { Id = employee.Id, @@ -49,7 +49,7 @@ public static string GetDepartmentName(Employee employee) => }; // Mapping that exercises nested multi-param helper - [Expressive] + [Projectable] public static EmployeeDto MapToDtoWithEmail(Employee employee) => new() { Id = employee.Id, @@ -63,7 +63,7 @@ public static string GetDepartmentName(Employee employee) => // ────────────────────────────────────────────────────────────────── // 2. Named-argument mapper — arguments passed out of order // ────────────────────────────────────────────────────────────────── -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class NamedArgEmployeeMapper { public static string FormatName(string first, string last) => @@ -72,7 +72,7 @@ public static string FormatName(string first, string last) => public static string GetDepartmentName(Employee employee) => employee.Department?.Name ?? "Unassigned"; - [Expressive] + [Projectable] public static EmployeeDto MapToDto(Employee employee) => new() { Id = employee.Id, @@ -88,7 +88,7 @@ public static string GetDepartmentName(Employee employee) => // 3. Updatable mapper with multi-param helpers // (exercises the BinaryExpressionSyntax spacing fix) // ────────────────────────────────────────────────────────────────── -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class MultiParamUpdatableMapper { public static string FormatName(string first, string last) => @@ -112,11 +112,11 @@ public static string GetDepartmentName(Employee employee) => } // ────────────────────────────────────────────────────────────────── -// 4. Multi-parameter [Expressive] method itself +// 4. Multi-parameter [Projectable] method itself // Generates Expression> MapWithYearExpression(int currentYear) // ────────────────────────────────────────────────────────────────── -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] -public static partial class MultiParamExpressiveMapper +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +public static partial class MultiParamProjectableMapper { public static string FormatName(string first, string last) => first + " " + last; @@ -124,8 +124,8 @@ public static string FormatName(string first, string last) => public static string GetDepartmentName(Employee employee) => employee.Department?.Name ?? "Unassigned"; - // The [Expressive] method ITSELF takes two parameters - [Expressive] + // The [Projectable] method ITSELF takes two parameters + [Projectable] public static EmployeeDto MapWithYear(Employee employee, int currentYear) => new() { Id = employee.Id, diff --git a/tests/AlephMapper.IntegrationTests/MultiParamTests.cs b/tests/AlephMapper.IntegrationTests/MultiParamTests.cs index dcad37e..748d490 100644 --- a/tests/AlephMapper.IntegrationTests/MultiParamTests.cs +++ b/tests/AlephMapper.IntegrationTests/MultiParamTests.cs @@ -1,4 +1,4 @@ -using AgileObjects.ReadableExpressions; +using AgileObjects.ReadableExpressions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -30,7 +30,7 @@ public async Task Cleanup() await _connection.DisposeAsync(); } - #region Multi-Param Expressive Tests + #region Multi-Param Projectable Tests [Test] public async Task MultiParam_FormatName_Expression_Should_Work_In_EFCore_Query() @@ -226,13 +226,13 @@ public async Task MultiParam_Updatable_Should_Preserve_Unmapped_Properties() #endregion - #region Multi-Param Expressive Method (method itself takes multiple params) + #region Multi-Param Projectable Method (method itself takes multiple params) [Test] - public async Task MultiParam_Expressive_Method_Should_Generate_Correct_Expression() + public async Task MultiParam_Projectable_Method_Should_Generate_Correct_Expression() { // Arrange — MapWithYear takes (Employee, int) and generates a single-parameter projection factory - var expression = MultiParamExpressiveMapper.MapWithYearExpression(2026); + var expression = MultiParamProjectableMapper.MapWithYearExpression(2026); // Act var result = await _context.Employees @@ -250,11 +250,11 @@ public async Task MultiParam_Expressive_Method_Should_Generate_Correct_Expressio } [Test] - public async Task MultiParam_Expressive_Method_Should_Have_Correct_Expression_Structure() + public async Task MultiParam_Projectable_Method_Should_Have_Correct_Expression_Structure() { // Arrange var currentYear = 2026; - var expression = MultiParamExpressiveMapper.MapWithYearExpression(currentYear); + var expression = MultiParamProjectableMapper.MapWithYearExpression(currentYear); var readable = expression.ToReadableString(); // Assert — the expression should reference both parameters diff --git a/tests/AlephMapper.IntegrationTests/README.md b/tests/AlephMapper.IntegrationTests/README.md index 359dd18..889d2d7 100644 --- a/tests/AlephMapper.IntegrationTests/README.md +++ b/tests/AlephMapper.IntegrationTests/README.md @@ -4,7 +4,7 @@ This test project provides comprehensive coverage of the AlephMapper source gene ## Test Coverage -### 1. Expressive Mapping Tests (`SimpleIntegrationTests`) +### 1. Projectable Mapping Tests (`SimpleIntegrationTests`) #### Basic Functionality - **Simple Property Expressions**: Tests basic property mapping with string interpolation @@ -56,13 +56,13 @@ The test project uses a comprehensive domain model representing an employee mana ### Mappers Tested -#### `SimpleEmployeeMapper` (Expressive with Rewrite Policy) +#### `SimpleEmployeeMapper` (Projectable with Rewrite Policy) - Basic property mapping - Null conditional operators with rewrite - Simple DTO creation - Method inlining demonstration -#### `SimpleIgnoreMapper` (Expressive with Ignore Policy) +#### `SimpleIgnoreMapper` (Projectable with Ignore Policy) - Same functionality as above but with ignore policy - Demonstrates different null handling behavior @@ -74,12 +74,12 @@ The test project uses a comprehensive domain model representing an employee mana ## Key Features Demonstrated ### 1. Expression Tree Generation -All Expressive mappers automatically generate corresponding expression tree methods: +All Projectable mappers automatically generate corresponding expression tree methods: - `GetFullName(Employee)` ? `GetFullNameExpression()` - `MapToSimpleDto(Employee)` ? `MapToSimpleDtoExpression()` ### 2. Method Inlining -When using Expressive mappers, method calls to other methods in the same class are automatically inlined: +When using Projectable mappers, method calls to other methods in the same class are automatically inlined: ```csharp public static EmployeeSimpleDto MapToSimpleDto(Employee employee) => new EmployeeSimpleDto { diff --git a/tests/AlephMapper.IntegrationTests/SimpleMappers.cs b/tests/AlephMapper.IntegrationTests/SimpleMappers.cs index dfe39d1..d76ba20 100644 --- a/tests/AlephMapper.IntegrationTests/SimpleMappers.cs +++ b/tests/AlephMapper.IntegrationTests/SimpleMappers.cs @@ -1,6 +1,6 @@ namespace AlephMapper.IntegrationTests; -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class SimpleEmployeeMapper { // Basic property mapping @@ -41,14 +41,14 @@ public static int GetAddressCount(Employee employee) => }; } -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static partial class SimpleIgnoreMapper { public static string GetFullName(Employee employee) => $"{employee.FirstName} {employee.LastName}"; public static string GetDepartmentName(Employee employee) => - employee.Department?.Name ?? "No Department"; + employee.Department!.Name ?? "No Department"; public static EmployeeSimpleDto MapToSimpleDto(Employee employee) => new() { @@ -75,8 +75,8 @@ public static partial class SimpleUpdateMapper public static DepartmentUpdateDto MapToDepartmentDto(Department department) => new() { Id = department.Id, - Name = department.Name, + Name = department.Name ?? "", Description = department.Description, IsActive = department.IsActive }; -} \ No newline at end of file +} diff --git a/tests/AlephMapper.IntegrationTests/SimpleTests.cs b/tests/AlephMapper.IntegrationTests/SimpleTests.cs index a143572..81b9f03 100644 --- a/tests/AlephMapper.IntegrationTests/SimpleTests.cs +++ b/tests/AlephMapper.IntegrationTests/SimpleTests.cs @@ -30,7 +30,7 @@ public async Task Cleanup() await _connection.DisposeAsync(); } - #region Expressive Tests + #region Projectable Tests [Test] public async Task Simple_Property_Expressions_Should_Work() diff --git a/tests/AlephMapper.IntegrationTests/UpdatableMappers.cs b/tests/AlephMapper.IntegrationTests/UpdatableMappers.cs index 4b7e8fc..3295f4e 100644 --- a/tests/AlephMapper.IntegrationTests/UpdatableMappers.cs +++ b/tests/AlephMapper.IntegrationTests/UpdatableMappers.cs @@ -6,7 +6,7 @@ public static partial class ConditionalUpdateMapper { // This method mimics the PersonMapper pattern: source == null ? null : new TargetType { ... } public static EmployeeSimpleDto ConditionalMapping(Employee? employee) => - employee == null ? null : new EmployeeSimpleDto + employee == null ? null! : new EmployeeSimpleDto { Id = employee.Id, FirstName = employee.FirstName, @@ -20,10 +20,10 @@ public static DepartmentUpdateDto ConditionalDepartmentMapping(Department? depar department != null ? new DepartmentUpdateDto { Id = department.Id, - Name = department.Name, + Name = department.Name ?? "", Description = department.Description, IsActive = department.IsActive - } : null; + } : null!; } // Mappers testing Updatable functionality @@ -56,7 +56,7 @@ public static partial class EmployeeUpdateMapper public static DepartmentUpdateDto UpdateDepartment(Department department) => new DepartmentUpdateDto { Id = department.Id, - Name = department.Name, + Name = department.Name ?? "", Description = department.Description, IsActive = department.IsActive }; @@ -155,11 +155,11 @@ public static partial class EmployeeUpdateMapper }; } -// Test both Expressive and Updatable on the same class -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +// Test both Projectable and Updatable on the same class +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class EmployeeCombinedMapper { - // Expressive methods + // Projectable methods public static string GetFullName(Employee employee) => $"{employee.FirstName} {employee.LastName}"; @@ -197,7 +197,7 @@ public static string GetDepartmentName(Employee employee) => Department = employee.Department != null ? new DepartmentUpdateDto { Id = employee.Department.Id, - Name = employee.Department.Name, + Name = employee.Department.Name ?? "", Description = employee.Department.Description, IsActive = employee.Department.IsActive } : null @@ -229,4 +229,4 @@ public static string GetDepartmentName(Employee employee) => IsActive = dto.Department.IsActive } : null }; -} \ No newline at end of file +} diff --git a/tests/AlephMapper.IntegrationTests/UpdatableTests.cs b/tests/AlephMapper.IntegrationTests/UpdatableTests.cs index b0fea96..4a24501 100644 --- a/tests/AlephMapper.IntegrationTests/UpdatableTests.cs +++ b/tests/AlephMapper.IntegrationTests/UpdatableTests.cs @@ -368,10 +368,10 @@ public async Task Nested_Update_Should_Handle_Partial_Null_Chains() #endregion - #region Combined Expressive and Updatable Tests + #region Combined Projectable and Updatable Tests [Test] - public async Task Combined_Mapper_Expressive_Methods_Should_Work() + public async Task Combined_Mapper_Projectable_Methods_Should_Work() { // Arrange var fullNameExpression = EmployeeCombinedMapper.GetFullNameExpression(); @@ -468,7 +468,7 @@ public async Task Conditional_Expression_Update_Should_Work_With_Null_Check() }; // Act - var result = ConditionalUpdateMapper.ConditionalMapping(sourceEmployee, targetDto); + var result = ConditionalUpdateMapper.ConditionalMapping(sourceEmployee!, targetDto); // Assert await Assert.That(result).IsSameReferenceAs(targetDto); @@ -493,7 +493,7 @@ public async Task Conditional_Expression_Update_Should_Handle_Null_Source() }; // Act - var result = ConditionalUpdateMapper.ConditionalMapping(sourceEmployee, targetDto); + var result = ConditionalUpdateMapper.ConditionalMapping(sourceEmployee!, targetDto); // Assert - When source is null, target should remain unchanged await Assert.That(result).IsSameReferenceAs(targetDto); @@ -523,7 +523,7 @@ public async Task Conditional_Expression_Update_Should_Work_With_Inverted_Condit }; // Act - var result = ConditionalUpdateMapper.ConditionalDepartmentMapping(sourceDepartment, targetDto); + var result = ConditionalUpdateMapper.ConditionalDepartmentMapping(sourceDepartment!, targetDto); // Assert await Assert.That(result).IsSameReferenceAs(targetDto); @@ -546,7 +546,7 @@ public async Task Conditional_Expression_Update_Should_Handle_Null_Source_Invert }; // Act - var result = ConditionalUpdateMapper.ConditionalDepartmentMapping(sourceDepartment, targetDto); + var result = ConditionalUpdateMapper.ConditionalDepartmentMapping(sourceDepartment!, targetDto); // Assert - When source is null, target should remain unchanged await Assert.That(result).IsSameReferenceAs(targetDto); @@ -628,4 +628,4 @@ public async Task Update_Should_Preserve_Collections_If_Present() } #endregion -} \ No newline at end of file +} diff --git a/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/Tests_PersonMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/Tests_PersonMapper_GeneratedMappings.g.cs index 846facc..025b918 100644 --- a/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/Tests_PersonMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/AdaptBoth/Expected/Tests_PersonMapper_GeneratedMappings.g.cs @@ -1,33 +1,38 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class PersonMapper { +#nullable disable /// - /// This is an auto-generated adapted mapping method for . + /// This is an auto-generated adapted mapping method for . /// - public static EmployeeDto MapEmployee(Employee source) => - new EmployeeDto + public static global::Tests.EmployeeDto MapEmployee(global::Tests.Employee source) => + new global::Tests.EmployeeDto { Id = source.Id, Name = source.FirstName + " " + source.LastName, Email = source.Email }; +#nullable restore +#nullable disable /// - /// This is an auto-generated adapted expression companion for . + /// This is an auto-generated adapted expression companion for . /// - public static Expression> MapEmployeeExpression() => - source => new EmployeeDto + public static global::System.Linq.Expressions.Expression> MapEmployeeExpression() => + source => new global::Tests.EmployeeDto { Id = source.Id, Name = source.FirstName + " " + source.LastName, Email = source.Email }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/AdaptNested/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/AdaptNested/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/AdaptNested/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/AdaptNested/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/AdaptNested/Expected/Tests_PersonMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/AdaptNested/Expected/Tests_PersonMapper_GeneratedMappings.g.cs index e4c889c..31b20cb 100644 --- a/tests/AlephMapper.Tests/Files/AdaptNested/Expected/Tests_PersonMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/AdaptNested/Expected/Tests_PersonMapper_GeneratedMappings.g.cs @@ -1,59 +1,65 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class PersonMapper { +#nullable disable /// - /// This is an auto-generated adapted mapping method for . + /// This is an auto-generated adapted mapping method for . /// - public static EmployeeDto MapEmployee(Employee source, bool includeEmail) => + public static global::Tests.EmployeeDto MapEmployee(global::Tests.Employee source, bool includeEmail) => includeEmail - ? new EmployeeDto + ? new global::Tests.EmployeeDto { Id = source.Id, Name = source.FirstName + " " + source.LastName, Email = source.Email } - : new EmployeeDto + : new global::Tests.EmployeeDto { Id = source.Id, Name = source.FirstName + " " + source.LastName, Email = string.Empty }; +#nullable restore +#nullable disable /// - /// This is an auto-generated adapted expression companion for . + /// This is an auto-generated adapted expression companion for . /// - public static Expression> MapEmployeeExpression(bool includeEmail) => + public static global::System.Linq.Expressions.Expression> MapEmployeeExpression(bool includeEmail) => source => includeEmail - ? new EmployeeDto + ? new global::Tests.EmployeeDto { Id = source.Id, Name = source.FirstName + " " + source.LastName, Email = source.Email } - : new EmployeeDto + : new global::Tests.EmployeeDto { Id = source.Id, Name = source.FirstName + " " + source.LastName, Email = string.Empty }; +#nullable restore +#nullable disable /// - /// This is an auto-generated adapted expression companion for . + /// This is an auto-generated adapted expression companion for . /// - public static Expression> MapEmployeeWithDetailExpression(string userLanguageCode) => - source => new EmployeeWithDetailDto + public static global::System.Linq.Expressions.Expression> MapEmployeeWithDetailExpression(string userLanguageCode) => + source => new global::Tests.EmployeeWithDetailDto { Id = source.Id, Details = source.Details - .Select(detail => new DetailDto + .Select(detail => new global::Tests.DetailDto { Code = detail.Code, Description = detail.Descriptions @@ -63,4 +69,5 @@ public static Expression> MapEmp }) .ToList() }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/Tests_AdaptUpdateMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/Tests_AdaptUpdateMapper_GeneratedMappings.g.cs index d99b1f0..bf99033 100644 --- a/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/Tests_AdaptUpdateMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/AdaptUpdate/Expected/Tests_AdaptUpdateMapper_GeneratedMappings.g.cs @@ -1,34 +1,39 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class AdaptUpdateMapper { +#nullable disable /// - /// This is an auto-generated adapted mapping method for . + /// This is an auto-generated adapted mapping method for . /// - public static Employee MapEmployee(EmployeeUpdateDto source, string prefix) => - new Employee + public static global::Tests.Employee MapEmployee(global::Tests.EmployeeUpdateDto source, string prefix) => + new global::Tests.Employee { Name = prefix + source.Name, Email = source.Email }; +#nullable restore +#nullable disable /// - /// This is an auto-generated adapted update method for . + /// This is an auto-generated adapted update method for . /// - public static Employee MapEmployee(EmployeeUpdateDto source, string prefix, Employee dest) + public static global::Tests.Employee MapEmployee(global::Tests.EmployeeUpdateDto source, string prefix, global::Tests.Employee dest) { if (source == null) return dest; if (dest == null) - dest = new Employee(); + dest = new global::Tests.Employee(); dest.Name = prefix + source.Name; dest.Email = source.Email; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/AnySourcesGeneratesAttribute/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/AnySourcesGeneratesAttribute/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/AnySourcesGeneratesAttribute/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/AnySourcesGeneratesAttribute/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper_Tests_CircularMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper_Tests_CircularMapper_GeneratedMappings.g.cs index 31670c3..6ee1325 100644 --- a/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper_Tests_CircularMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/CircularMapper/Expected/AlephMapper_Tests_CircularMapper_GeneratedMappings.g.cs @@ -1,50 +1,57 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class CircularMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ProcessValueExpression() => + public static global::System.Linq.Expressions.Expression> ProcessValueExpression() => source => source.Value.ToUpper() ?? ""; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> UpdateSimpleDtoExpression() => - source => new CircularDto + public static global::System.Linq.Expressions.Expression> UpdateSimpleDtoExpression() => + source => new global::AlephMapper.Tests.CircularDto { ProcessedValue = source.Value.ToUpper() ?? "" // Direct assignment without method call }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static CircularDto UpdateSimpleDto(CircularTestModel source, CircularDto dest) + public static global::AlephMapper.Tests.CircularDto UpdateSimpleDto(global::AlephMapper.Tests.CircularTestModel source, global::AlephMapper.Tests.CircularDto dest) { if (source == null) return dest; if (dest == null) - dest = new CircularDto(); + dest = new global::AlephMapper.Tests.CircularDto(); dest.ProcessedValue = source?.Value?.ToUpper() ?? ""; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/CircularMapper/Sources/CircularMapper.cs b/tests/AlephMapper.Tests/Files/CircularMapper/Sources/CircularMapper.cs index 921e85e..6e4c02d 100644 --- a/tests/AlephMapper.Tests/Files/CircularMapper/Sources/CircularMapper.cs +++ b/tests/AlephMapper.Tests/Files/CircularMapper/Sources/CircularMapper.cs @@ -1,7 +1,7 @@ -namespace AlephMapper.Tests; +namespace AlephMapper.Tests; // Mapper with intentional circular references to test detection -[Expressive] +[Projectable] internal static partial class CircularMapper { // Simple direct circular reference - this method calls itself diff --git a/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper_Tests_CircularPropertyMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper_Tests_CircularPropertyMapper_GeneratedMappings.g.cs index 5b50265..9df9f1a 100644 --- a/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper_Tests_CircularPropertyMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Expected/AlephMapper_Tests_CircularPropertyMapper_GeneratedMappings.g.cs @@ -1,51 +1,56 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class CircularPropertyMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> UpdateTypeAExpression() => - source => new TypeA + public static global::System.Linq.Expressions.Expression> UpdateTypeAExpression() => + source => new global::AlephMapper.Tests.TypeA { Name = source.Name, - B = new TypeB + B = new global::AlephMapper.Tests.TypeB { - A = new TypeA + A = new global::AlephMapper.Tests.TypeA { Name = source.Name } } }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static TypeA UpdateTypeA(CircularPropsSource source, TypeA dest) + public static global::AlephMapper.Tests.TypeA UpdateTypeA(global::AlephMapper.Tests.CircularPropsSource source, global::AlephMapper.Tests.TypeA dest) { if (source == null) return dest; if (dest == null) - dest = new TypeA(); + dest = new global::AlephMapper.Tests.TypeA(); dest.Name = source?.Name; if (dest.B == null) - dest.B = new TypeB(); + dest.B = new global::AlephMapper.Tests.TypeB(); if (dest.B.A == null) - dest.B.A = new TypeA(); + dest.B.A = new global::AlephMapper.Tests.TypeA(); dest.B.A.Name = source?.Name; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Sources/CircularPropertyMapper.cs b/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Sources/CircularPropertyMapper.cs index ea14e4f..4aa772a 100644 --- a/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Sources/CircularPropertyMapper.cs +++ b/tests/AlephMapper.Tests/Files/CircularPropertyMapper/Sources/CircularPropertyMapper.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace AlephMapper.Tests; -[Expressive] +[Projectable] internal static partial class CircularPropertyMapper { [Updatable] diff --git a/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper_Tests_CollectionMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper_Tests_CollectionMapper_GeneratedMappings.g.cs index 7c543f1..911f563 100644 --- a/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper_Tests_CollectionMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/CollectionMapper/Expected/AlephMapper_Tests_CollectionMapper_GeneratedMappings.g.cs @@ -1,55 +1,59 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class CollectionMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestWithCollectionsExpression() => - source => new DestWithCollections + public static global::System.Linq.Expressions.Expression> MapToDestWithCollectionsExpression() => + source => new global::AlephMapper.Tests.DestWithCollections { Name = source.Name, Tags = source.Tags, Categories = source.Categories, NestedObject = source.NestedObject != null - ? new NestedModel + ? new global::AlephMapper.Tests.NestedModel { Value = source.NestedObject.Value, NestedList = source.NestedObject.NestedList } : null }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestWithCollections MapToDestWithCollections(SourceWithCollections source, DestWithCollections dest) + public static global::AlephMapper.Tests.DestWithCollections MapToDestWithCollections(global::AlephMapper.Tests.SourceWithCollections source, global::AlephMapper.Tests.DestWithCollections dest) { if (source == null) return dest; if (dest == null) - dest = new DestWithCollections(); + dest = new global::AlephMapper.Tests.DestWithCollections(); dest.Name = source.Name; // Skipping collection property: dest.Tags // Skipping collection property: dest.Categories if (source.NestedObject != null) { if (dest.NestedObject == null) - dest.NestedObject = new NestedModel(); + dest.NestedObject = new global::AlephMapper.Tests.NestedModel(); dest.NestedObject.Value = source.NestedObject.Value; // Skipping collection property: dest.NestedObject.NestedList } @@ -59,43 +63,47 @@ public static DestWithCollections MapToDestWithCollections(SourceWithCollections } return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestSimpleExpression() => - source => new DestWithCollections + public static global::System.Linq.Expressions.Expression> MapToDestSimpleExpression() => + source => new global::AlephMapper.Tests.DestWithCollections { Name = source.Name, NestedObject = source.NestedObject != null - ? new NestedModel + ? new global::AlephMapper.Tests.NestedModel { Value = source.NestedObject.Value } : null }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestWithCollections MapToDestSimple(SourceWithCollections source, DestWithCollections dest) + public static global::AlephMapper.Tests.DestWithCollections MapToDestSimple(global::AlephMapper.Tests.SourceWithCollections source, global::AlephMapper.Tests.DestWithCollections dest) { if (source == null) return dest; if (dest == null) - dest = new DestWithCollections(); + dest = new global::AlephMapper.Tests.DestWithCollections(); dest.Name = source.Name; if (source.NestedObject != null) { if (dest.NestedObject == null) - dest.NestedObject = new NestedModel(); + dest.NestedObject = new global::AlephMapper.Tests.NestedModel(); dest.NestedObject.Value = source.NestedObject.Value; } else @@ -104,4 +112,5 @@ public static DestWithCollections MapToDestSimple(SourceWithCollections source, } return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/CollectionMapper/Sources/CollectionMapper.cs b/tests/AlephMapper.Tests/Files/CollectionMapper/Sources/CollectionMapper.cs index 754946e..3af4e78 100644 --- a/tests/AlephMapper.Tests/Files/CollectionMapper/Sources/CollectionMapper.cs +++ b/tests/AlephMapper.Tests/Files/CollectionMapper/Sources/CollectionMapper.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace AlephMapper.Tests; @@ -30,7 +30,7 @@ public class NestedModel } // Mapper with collection properties to test skipping behavior -[Expressive] +[Projectable] internal static partial class CollectionMapper { [Updatable] diff --git a/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper_Tests_ConditionalPatternMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper_Tests_ConditionalPatternMapper_GeneratedMappings.g.cs index b847600..ef2638f 100644 --- a/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper_Tests_ConditionalPatternMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ConditionalPatternMapper/Expected/AlephMapper_Tests_ConditionalPatternMapper_GeneratedMappings.g.cs @@ -1,58 +1,62 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ConditionalPatternMapper { +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestModel BothSidesObjects(SourceModel source, DestModel dest) + public static global::AlephMapper.Tests.DestModel BothSidesObjects(global::AlephMapper.Tests.SourceModel source, global::AlephMapper.Tests.DestModel dest) { if (source == null) return dest; if (dest == null) - dest = new DestModel(); + dest = new global::AlephMapper.Tests.DestModel(); dest.Name = source.Name; if (source.Value == null) { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = "Default"; dest.Nested.Number = 0; } else { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = source.Name; dest.Nested.Number = source.Value.Value; } return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestModel ObjectThenNull(SourceModel source, DestModel dest) + public static global::AlephMapper.Tests.DestModel ObjectThenNull(global::AlephMapper.Tests.SourceModel source, global::AlephMapper.Tests.DestModel dest) { if (source == null) return dest; if (dest == null) - dest = new DestModel(); + dest = new global::AlephMapper.Tests.DestModel(); dest.Name = source.Name; if (source.Name != null) { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = source.Name; dest.Nested.Number = 42; } @@ -62,18 +66,20 @@ public static DestModel ObjectThenNull(SourceModel source, DestModel dest) } return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestModel NullThenObject(SourceModel source, DestModel dest) + public static global::AlephMapper.Tests.DestModel NullThenObject(global::AlephMapper.Tests.SourceModel source, global::AlephMapper.Tests.DestModel dest) { if (source == null) return dest; if (dest == null) - dest = new DestModel(); + dest = new global::AlephMapper.Tests.DestModel(); dest.Name = source.Name; if (source.Name == null) { @@ -82,91 +88,98 @@ public static DestModel NullThenObject(SourceModel source, DestModel dest) else { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = source.Name; dest.Nested.Number = 42; } return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestModel NestedBothSides(SourceModel source, DestModel dest) + public static global::AlephMapper.Tests.DestModel NestedBothSides(global::AlephMapper.Tests.SourceModel source, global::AlephMapper.Tests.DestModel dest) { if (source == null) return dest; if (dest == null) - dest = new DestModel(); + dest = new global::AlephMapper.Tests.DestModel(); dest.Name = source.Name; if (source.Nested?.Content == null) { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = "Fallback"; dest.Nested.Number = -1; } else { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = source.Nested.Content; dest.Nested.Number = source.Nested.Number; } return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestModel ObjectThenThrow(SourceModel source, DestModel dest) + public static global::AlephMapper.Tests.DestModel ObjectThenThrow(global::AlephMapper.Tests.SourceModel source, global::AlephMapper.Tests.DestModel dest) { if (source == null) return dest; if (dest == null) - dest = new DestModel(); + dest = new global::AlephMapper.Tests.DestModel(); dest.Name = source.Name; if (source.Name != null) { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = source.Name; dest.Nested.Number = 42; } else { - throw new ArgumentNullException(nameof(source.Name)); + throw new global::System.ArgumentNullException(nameof(source.Name)); } return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestModel ThrowThenObject(SourceModel source, DestModel dest) + public static global::AlephMapper.Tests.DestModel ThrowThenObject(global::AlephMapper.Tests.SourceModel source, global::AlephMapper.Tests.DestModel dest) { if (source == null) return dest; if (dest == null) - dest = new DestModel(); + dest = new global::AlephMapper.Tests.DestModel(); dest.Name = source.Name; if (source.Name == null) { - throw new ArgumentNullException(nameof(source.Name)); + throw new global::System.ArgumentNullException(nameof(source.Name)); } else { if (dest.Nested == null) - dest.Nested = new NestedDest(); + dest.Nested = new global::AlephMapper.Tests.NestedDest(); dest.Nested.Content = source.Name; dest.Nested.Number = 42; } return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreIgnoreMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreIgnoreMapper_GeneratedMappings.g.cs index 0837d97..f2f52da 100644 --- a/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreIgnoreMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreIgnoreMapper_GeneratedMappings.g.cs @@ -1,54 +1,63 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class EfCoreIgnoreMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> GetPersonNameExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonNameExpression() => person => person.Name; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> GetPersonEmailExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonEmailExpression() => person => person.Email; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> GetPersonAgeExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonAgeExpression() => person => person.BirthInfo.Age; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> GetBirthPlaceExpression() => + public static global::System.Linq.Expressions.Expression> GetBirthPlaceExpression() => person => person.BirthInfo.BirthPlace ?? "Unknown"; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreMapper_GeneratedMappings.g.cs index 794969d..9a11c53 100644 --- a/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/EfCoreMapper/Expected/AlephMapper_Tests_EfCoreMapper_GeneratedMappings.g.cs @@ -1,23 +1,25 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class EfCoreMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetPersonComplexExpression() => - p => new PersonSummaryDto + public static global::System.Linq.Expressions.Expression> GetPersonComplexExpression() => + p => new global::AlephMapper.Tests.PersonSummaryDto { Id = p.Id, Name = p.Name, @@ -49,211 +51,245 @@ public static Expression> GetPersonComplexExpress ? p.Name + " (" + p.BirthInfo.Age + " years old) from " + (p.BirthInfo.BirthPlace ?? "Unknown") : p.Name + " (unknown age) from Unknown" }; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetPersonNameExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonNameExpression() => person => person.Name; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetPersonEmailExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonEmailExpression() => person => person.Email; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetPersonAgeExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonAgeExpression() => person => (person.BirthInfo != null ? (person.BirthInfo.Age) : (int?)null); +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> IsOlderThan30Expression() => + public static global::System.Linq.Expressions.Expression> IsOlderThan30Expression() => person => (person.BirthInfo != null ? (person.BirthInfo.Age) : (int?)null) > 30; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetBirthPlaceExpression() => + public static global::System.Linq.Expressions.Expression> GetBirthPlaceExpression() => person => (person.BirthInfo != null ? (person.BirthInfo.BirthPlace) : (string)null) ?? "Unknown"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetBirthAddressExpression() => + public static global::System.Linq.Expressions.Expression> GetBirthAddressExpression() => person => (person.BirthInfo != null ? (person.BirthInfo.Address) : (string)null) ?? "Not specified"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> HasBirthInfoExpression() => + public static global::System.Linq.Expressions.Expression> HasBirthInfoExpression() => person => person.BirthInfo != null; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> IsAdultExpression() => + public static global::System.Linq.Expressions.Expression> IsAdultExpression() => person => (person.BirthInfo != null ? (person.BirthInfo.Age) : (int?)null) >= 18; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> BornInUkraineExpression() => + public static global::System.Linq.Expressions.Expression> BornInUkraineExpression() => person => (person.BirthInfo != null ? (person.BirthInfo.Address) : (string)null) != null && person.BirthInfo.Address.Contains("Ukraine"); +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetAddressCountExpression() => + public static global::System.Linq.Expressions.Expression> GetAddressCountExpression() => person => person.Addresses.Count; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetOrderCountExpression() => + public static global::System.Linq.Expressions.Expression> GetOrderCountExpression() => person => person.Orders.Count; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> HasActiveAddressExpression() => + public static global::System.Linq.Expressions.Expression> HasActiveAddressExpression() => person => person.Addresses.Any(a => a.IsActive); +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> HasCompletedOrdersExpression() => + public static global::System.Linq.Expressions.Expression> HasCompletedOrdersExpression() => person => person.Orders.Any(o => o.IsCompleted); +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetTotalOrderAmountExpression() => + public static global::System.Linq.Expressions.Expression> GetTotalOrderAmountExpression() => person => person.Orders .Where(o => o.IsCompleted) .Sum(o => o.Amount); +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetPersonSummaryExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonSummaryExpression() => person => person.BirthInfo != null ? person.Name + " (" + person.BirthInfo.Age + " years old) from " + (person.BirthInfo.BirthPlace ?? "Unknown") : person.Name + " (unknown age) from Unknown"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> LivesInSamePlaceAsBornExpression() => + public static global::System.Linq.Expressions.Expression> LivesInSamePlaceAsBornExpression() => person => (person.BirthInfo != null ? (person.BirthInfo.BirthPlace) : (string)null) != null && person.Addresses.Any(a => a.IsActive && a.City == person.BirthInfo.BirthPlace); +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetPersonCategoryExpression() => + public static global::System.Linq.Expressions.Expression> GetPersonCategoryExpression() => person => person.BirthInfo == null ? "Unknown Age" : person.BirthInfo.Age < 18 @@ -261,17 +297,20 @@ public static Expression> GetPersonCategoryExpression() => : person.BirthInfo.Age < 65 ? "Adult" : "Senior"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> IsVipCustomerExpression() => + public static global::System.Linq.Expressions.Expression> IsVipCustomerExpression() => person => person.Orders .Where(o => o.IsCompleted) .Sum(o => o.Amount) >= 1000m; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/EfCoreMapper/Sources/EfCoreMappers.cs b/tests/AlephMapper.Tests/Files/EfCoreMapper/Sources/EfCoreMappers.cs index aca1c27..724759d 100644 --- a/tests/AlephMapper.Tests/Files/EfCoreMapper/Sources/EfCoreMappers.cs +++ b/tests/AlephMapper.Tests/Files/EfCoreMapper/Sources/EfCoreMappers.cs @@ -1,8 +1,8 @@ -namespace AlephMapper.Tests; +namespace AlephMapper.Tests; // Projection mappers for EF Core integration tests -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class EfCoreMapper { public static PersonSummaryDto GetPersonComplex(Person p) => new PersonSummaryDto @@ -88,7 +88,7 @@ public static bool IsVipCustomer(Person person) => } // Mapper with Ignore policy for comparison -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static partial class EfCoreIgnoreMapper { public static string GetPersonName(Person person) => person.Name; diff --git a/tests/AlephMapper.Tests/Files/Expressive/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/Expressive/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/Expressive/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/Expressive/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/Expressive/Expected/Tests_SampleMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/Expressive/Expected/Tests_SampleMapper_GeneratedMappings.g.cs index e0abbab..c57a17b 100644 --- a/tests/AlephMapper.Tests/Files/Expressive/Expected/Tests_SampleMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/Expressive/Expected/Tests_SampleMapper_GeneratedMappings.g.cs @@ -1,22 +1,25 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class SampleMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ProjectNameExpression() => + public static global::System.Linq.Expressions.Expression> ProjectNameExpression() => source => source.Name; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/Expressive/Sources/Source.cs b/tests/AlephMapper.Tests/Files/Expressive/Sources/Source.cs index ad2adb2..77c9613 100644 --- a/tests/AlephMapper.Tests/Files/Expressive/Sources/Source.cs +++ b/tests/AlephMapper.Tests/Files/Expressive/Sources/Source.cs @@ -1,8 +1,8 @@ -using AlephMapper; +using AlephMapper; namespace Tests; -[Expressive] +[Projectable] public static partial class SampleMapper { public static string ProjectName(SampleSource source) => source.Name; diff --git a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ConditionalExtensionTestPersonMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ConditionalExtensionTestPersonMapper_GeneratedMappings.g.cs index 5d77c39..2552e45 100644 --- a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ConditionalExtensionTestPersonMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ConditionalExtensionTestPersonMapper_GeneratedMappings.g.cs @@ -1,28 +1,30 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ConditionalExtensionTestPersonMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - person => new ExtensionTestPersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.ExtensionTestPersonDto { Id = person.Id, Name = person.Name, - Address = new ExtensionTestAddressDto + Address = new global::AlephMapper.Tests.ExtensionTestAddressDto { Street = person.Address.Street, City = person.Address.City, @@ -30,4 +32,5 @@ public static Expression> ToDt FormattedAddress = $"{person.Address.Street}, {person.Address.City} {person.Address.PostalCode}" } }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestAddressMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestAddressMapper_GeneratedMappings.g.cs index 6a332ba..f44a055 100644 --- a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestAddressMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestAddressMapper_GeneratedMappings.g.cs @@ -1,28 +1,31 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ExtensionTestAddressMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - address => new ExtensionTestAddressDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + address => new global::AlephMapper.Tests.ExtensionTestAddressDto { Street = address.Street, City = address.City, PostalCode = address.PostalCode, FormattedAddress = $"{address.Street}, {address.City} {address.PostalCode}" }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestPersonMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestPersonMapper_GeneratedMappings.g.cs index 9805e9e..c467f31 100644 --- a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestPersonMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Expected/AlephMapper_Tests_ExtensionTestPersonMapper_GeneratedMappings.g.cs @@ -1,28 +1,30 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ExtensionTestPersonMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - person => new ExtensionTestPersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.ExtensionTestPersonDto { Id = person.Id, Name = person.Name, - Address = new ExtensionTestAddressDto + Address = new global::AlephMapper.Tests.ExtensionTestAddressDto { Street = person.Address.Street, City = person.Address.City, @@ -30,4 +32,5 @@ public static Expression> ToDt FormattedAddress = $"{person.Address.Street}, {person.Address.City} {person.Address.PostalCode}" } }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Sources/ExtensionMethodInliningTests.cs b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Sources/ExtensionMethodInliningTests.cs index 83edfcb..b187391 100644 --- a/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Sources/ExtensionMethodInliningTests.cs +++ b/tests/AlephMapper.Tests/Files/ExtensionMethodInlining/Sources/ExtensionMethodInliningTests.cs @@ -35,7 +35,7 @@ public class ExtensionTestPersonDto // Extension method mapper public static partial class ExtensionTestAddressMapper { - [Expressive] + [Projectable] public static ExtensionTestAddressDto ToDto(this ExtensionTestAddress address) => new() { Street = address.Street, @@ -48,7 +48,7 @@ public static partial class ExtensionTestAddressMapper // Main mapper that uses the extension method public static partial class ExtensionTestPersonMapper { - [Expressive] + [Projectable] public static ExtensionTestPersonDto ToDto(ExtensionTestPerson person) => new() { Id = person.Id, @@ -60,7 +60,7 @@ public static partial class ExtensionTestPersonMapper // Main mapper that uses conditional access extension method public static partial class ConditionalExtensionTestPersonMapper { - [Expressive] + [Projectable] public static ExtensionTestPersonDto ToDto(ExtensionTestPerson person) => new() { Id = person.Id, diff --git a/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper_Tests_Mapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper_Tests_Mapper_GeneratedMappings.g.cs index cbcd7dd..0ee4b8c 100644 --- a/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper_Tests_Mapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/Mapper/Expected/AlephMapper_Tests_Mapper_GeneratedMappings.g.cs @@ -1,71 +1,81 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class Mapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> BornInKyivAndOlder35Expression() => + public static global::System.Linq.Expressions.Expression> BornInKyivAndOlder35Expression() => source => source.BirthInfo.Address == "Kyiv" && source.BirthInfo.Age > 35 && source.BirthInfo.Age < 65; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> BornInKyivExpression() => + public static global::System.Linq.Expressions.Expression> BornInKyivExpression() => source => source.Address == "Kyiv"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> Younger65Expression() => + public static global::System.Linq.Expressions.Expression> Younger65Expression() => source => source.Age < 65; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> LivesInExpression() => + public static global::System.Linq.Expressions.Expression> LivesInExpression() => source => source.Address == "Kyiv"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestDtoExpression() => - source => new DestDto + public static global::System.Linq.Expressions.Expression> MapToDestDtoExpression() => + source => new global::AlephMapper.Tests.DestDto { Name = source.Name, BirthInfo = source.BirthInfo != null - ? new BirthInfoDto + ? new global::AlephMapper.Tests.BirthInfoDto { Age = source.BirthInfo.Age, Address = source.BirthInfo.Address @@ -73,23 +83,25 @@ public static Expression> MapToDestDtoExpression() => : null, ContactInfo = source.Email }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestDto MapToDestDto(SourceDto source, DestDto dest) + public static global::AlephMapper.Tests.DestDto MapToDestDto(global::AlephMapper.Tests.SourceDto source, global::AlephMapper.Tests.DestDto dest) { if (source == null) return dest; if (dest == null) - dest = new DestDto(); + dest = new global::AlephMapper.Tests.DestDto(); dest.Name = source.Name; if (source.BirthInfo != null) { if (dest.BirthInfo == null) - dest.BirthInfo = new BirthInfoDto(); + dest.BirthInfo = new global::AlephMapper.Tests.BirthInfoDto(); dest.BirthInfo.Age = source.BirthInfo.Age; dest.BirthInfo.Address = source.BirthInfo.Address; } @@ -100,40 +112,44 @@ public static DestDto MapToDestDto(SourceDto source, DestDto dest) dest.ContactInfo = source.Email; return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestDto1Expression() => - source => new DestDto + public static global::System.Linq.Expressions.Expression> MapToDestDto1Expression() => + source => new global::AlephMapper.Tests.DestDto { Name = source.Name, BirthInfo = source.BirthInfo == null ? null - : new BirthInfoDto + : new global::AlephMapper.Tests.BirthInfoDto { Age = source.BirthInfo.Age, Address = source.BirthInfo.Address }, ContactInfo = source.Email }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static DestDto MapToDestDto1(SourceDto source, DestDto dest) + public static global::AlephMapper.Tests.DestDto MapToDestDto1(global::AlephMapper.Tests.SourceDto source, global::AlephMapper.Tests.DestDto dest) { if (source == null) return dest; if (dest == null) - dest = new DestDto(); + dest = new global::AlephMapper.Tests.DestDto(); dest.Name = source.Name; if (source.BirthInfo == null) { @@ -142,26 +158,29 @@ public static DestDto MapToDestDto1(SourceDto source, DestDto dest) else { if (dest.BirthInfo == null) - dest.BirthInfo = new BirthInfoDto(); + dest.BirthInfo = new global::AlephMapper.Tests.BirthInfoDto(); dest.BirthInfo.Age = source.BirthInfo.Age; dest.BirthInfo.Address = source.BirthInfo.Address; } dest.ContactInfo = source.Email; return dest; } +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToBirthInfoDtoExpression() => - bi => new BirthInfoDto + public static global::System.Linq.Expressions.Expression> MapToBirthInfoDtoExpression() => + bi => new global::AlephMapper.Tests.BirthInfoDto { Age = bi.Age, Address = bi.Address }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/Mapper/Sources/Mapper.cs b/tests/AlephMapper.Tests/Files/Mapper/Sources/Mapper.cs index 3717cd2..72498c6 100644 --- a/tests/AlephMapper.Tests/Files/Mapper/Sources/Mapper.cs +++ b/tests/AlephMapper.Tests/Files/Mapper/Sources/Mapper.cs @@ -1,11 +1,11 @@ -namespace AlephMapper.Tests; +namespace AlephMapper.Tests; internal static class Mapper1 { public static bool Older35(BirthInfo? source) => source?.Age > 35; } -[Expressive] +[Projectable] internal static partial class Mapper { public static bool BornInKyivAndOlder35(SourceDto source) => BornInKyiv(source.BirthInfo) && Mapper1.Older35(source.BirthInfo) && Younger65(source.BirthInfo); diff --git a/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper_Tests_SimpleObjectMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper_Tests_SimpleObjectMapper_GeneratedMappings.g.cs index 7c279c3..50d064b 100644 --- a/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper_Tests_SimpleObjectMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MethodGroupSpec/Expected/AlephMapper_Tests_SimpleObjectMapper_GeneratedMappings.g.cs @@ -1,27 +1,30 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class SimpleObjectMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDtoExpression() => - so => new SimpleDto + public static global::System.Linq.Expressions.Expression> MapToDtoExpression() => + so => new global::AlephMapper.Tests.SimpleDto { Attributes = so.Attributes .Select(attr => attr.Name) - .ToList() ?? new List() + .ToList() ?? new global::System.Collections.Generic.List() }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MethodGroupSpec/Sources/MethodGroup.cs b/tests/AlephMapper.Tests/Files/MethodGroupSpec/Sources/MethodGroup.cs index bc33bfc..808b78e 100644 --- a/tests/AlephMapper.Tests/Files/MethodGroupSpec/Sources/MethodGroup.cs +++ b/tests/AlephMapper.Tests/Files/MethodGroupSpec/Sources/MethodGroup.cs @@ -1,4 +1,4 @@ -using AgileObjects.ReadableExpressions; +using AgileObjects.ReadableExpressions; namespace AlephMapper.Tests; @@ -19,7 +19,7 @@ internal class SimpleDto internal static partial class SimpleObjectMapper { - [Expressive] + [Projectable] public static SimpleDto MapToDto(SimpleObject so) => new SimpleDto { Attributes = so.Attributes?.Select(MapFromAttribute).ToList() ?? [] diff --git a/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper_Tests_MethodGroupToEntityList_PersonMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper_Tests_MethodGroupToEntityList_PersonMapper_GeneratedMappings.g.cs index e320be5..b132488 100644 --- a/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper_Tests_MethodGroupToEntityList_PersonMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Expected/AlephMapper_Tests_MethodGroupToEntityList_PersonMapper_GeneratedMappings.g.cs @@ -1,31 +1,34 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MethodGroupToEntityList; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class PersonMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToEntityExpression() => - dto => new Person + public static global::System.Linq.Expressions.Expression> ToEntityExpression() => + dto => new global::AlephMapper.Tests.MethodGroupToEntityList.Person { ContactNumbers = dto.PhoneNumbers - .Select(dto => new PhoneNumber + .Select(dto => new global::AlephMapper.Tests.MethodGroupToEntityList.PhoneNumber { Number = dto.Number }) .ToList() }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Sources/PersonMapper.cs b/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Sources/PersonMapper.cs index 8a3a553..f41981e 100644 --- a/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Sources/PersonMapper.cs +++ b/tests/AlephMapper.Tests/Files/MethodGroupToEntityList/Sources/PersonMapper.cs @@ -34,7 +34,7 @@ public static class PhoneMapper public static partial class PersonMapper { - [Expressive] + [Projectable] public static Person ToEntity(PersonDto dto) => new() { ContactNumbers = dto.PhoneNumbers.Select(PhoneMapper.ToEntity).ToList() diff --git a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperIgnore_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperIgnore_GeneratedMappings.g.cs index e4e2936..fe2fb07 100644 --- a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperIgnore_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperIgnore_GeneratedMappings.g.cs @@ -1,26 +1,29 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MultiParamExtensionInlining; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class PersonProductMapperIgnore { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - person => new PersonProductDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.MultiParamExtensionInlining.PersonProductDto { Name = person.Name, FavoritePrice = "$" + person.FavoriteProduct.Price }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperRewrite_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperRewrite_GeneratedMappings.g.cs index 33da943..9aab34d 100644 --- a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperRewrite_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_PersonProductMapperRewrite_GeneratedMappings.g.cs @@ -1,28 +1,31 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MultiParamExtensionInlining; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class PersonProductMapperRewrite { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> ToDtoExpression() => - person => new PersonProductDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.MultiParamExtensionInlining.PersonProductDto { Name = person.Name, FavoritePrice = (person.FavoriteProduct != null ? ("$" + person.FavoriteProduct.Price) : (string)null) }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_ProductMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_ProductMapper_GeneratedMappings.g.cs index 7f3971c..ef8a4bf 100644 --- a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_ProductMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Expected/AlephMapper_Tests_MultiParamExtensionInlining_ProductMapper_GeneratedMappings.g.cs @@ -1,27 +1,30 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MultiParamExtensionInlining; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ProductMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - product => new ProductDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + product => new global::AlephMapper.Tests.MultiParamExtensionInlining.ProductDto { Label = product.Name, PriceTag = "$" + product.Price, Total = product.Price * product.Quantity * (1 + 0.1m) - 5m }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Sources/MultiParamExtMapper.cs b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Sources/MultiParamExtMapper.cs index b5ca190..6fbd45a 100644 --- a/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Sources/MultiParamExtMapper.cs +++ b/tests/AlephMapper.Tests/Files/MultiParamExtensionInlining/Sources/MultiParamExtMapper.cs @@ -44,7 +44,7 @@ public static decimal ComputeTotal(this Product product, decimal taxRate, decima /// public static partial class ProductMapper { - [Expressive] + [Projectable] public static ProductDto ToDto(Product product) => new() { Label = product.Name, @@ -69,10 +69,10 @@ public class PersonProductDto public string FavoritePrice { get; set; } = string.Empty; } -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static partial class PersonProductMapperIgnore { - [Expressive] + [Projectable] public static PersonProductDto ToDto(Person person) => new() { Name = person.Name, @@ -80,10 +80,10 @@ public static partial class PersonProductMapperIgnore }; } -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class PersonProductMapperRewrite { - [Expressive] + [Projectable] public static PersonProductDto ToDto(Person person) => new() { Name = person.Name, diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_MultiParamExpressiveMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_MultiParamExpressiveMapper_GeneratedMappings.g.cs deleted file mode 100644 index 39bd9f2..0000000 --- a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_MultiParamExpressiveMapper_GeneratedMappings.g.cs +++ /dev/null @@ -1,26 +0,0 @@ -using AlephMapper; -using System; -using System.CodeDom.Compiler; -using System.Linq; -using System.Linq.Expressions; - -namespace AlephMapper.Tests.MultiParameterInlining; - -[GeneratedCode("AlephMapper", "0.6.2")] -partial class MultiParamExpressiveMapper -{ - /// - /// This is an auto-generated expression companion for . - /// - /// - /// - /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. - /// - /// - public static Expression> MapExpression(int currentYear) => - person => new PersonDto - { - FullName = person.First + " " + person.Last, - BirthYear = currentYear - person.Age - }; -} diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_MultiParamProjectableMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_MultiParamProjectableMapper_GeneratedMappings.g.cs new file mode 100644 index 0000000..8872dde --- /dev/null +++ b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_MultiParamProjectableMapper_GeneratedMappings.g.cs @@ -0,0 +1,29 @@ +// + +using AlephMapper; +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace AlephMapper.Tests.MultiParameterInlining; + +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] +partial class MultiParamProjectableMapper +{ +#nullable disable + /// + /// This is an auto-generated expression companion for . + /// + /// + /// + /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. + /// + /// + public static global::System.Linq.Expressions.Expression> MapExpression(int currentYear) => + person => new global::AlephMapper.Tests.MultiParameterInlining.PersonDto + { + FullName = person.First + " " + person.Last, + BirthYear = currentYear - person.Age + }; +#nullable restore +} diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NamedArgMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NamedArgMapper_GeneratedMappings.g.cs index 6d7a232..c24874b 100644 --- a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NamedArgMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NamedArgMapper_GeneratedMappings.g.cs @@ -1,25 +1,28 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MultiParameterInlining; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NamedArgMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - person => new PersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.MultiParameterInlining.PersonDto { FullName = person.First + " " + person.Last }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NestedMultiParamMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NestedMultiParamMapper_GeneratedMappings.g.cs index 025f784..fbde9d4 100644 --- a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NestedMultiParamMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_NestedMultiParamMapper_GeneratedMappings.g.cs @@ -1,25 +1,28 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MultiParameterInlining; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NestedMultiParamMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - person => new PersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.MultiParameterInlining.PersonDto { FullName = person.First + " " + person.Last + " (age " + person.Age + ")" }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_PersonMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_PersonMapper_GeneratedMappings.g.cs index d5b36e4..891790d 100644 --- a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_PersonMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_PersonMapper_GeneratedMappings.g.cs @@ -1,28 +1,31 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MultiParameterInlining; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class PersonMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - person => new PersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.MultiParameterInlining.PersonDto { FullName = person.First + " " + person.Last, Address = person.Street + ", " + person.City + " " + person.Zip, Description = person.First + " " + person.Last + " (age " + person.Age + ")", BirthYear = 2026 - person.Age }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_UpdatableMultiParamMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_UpdatableMultiParamMapper_GeneratedMappings.g.cs index 32cf9d6..c07b2e3 100644 --- a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_UpdatableMultiParamMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Expected/AlephMapper_Tests_MultiParameterInlining_UpdatableMultiParamMapper_GeneratedMappings.g.cs @@ -1,43 +1,48 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests.MultiParameterInlining; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class UpdatableMultiParamMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression(int currentYear) => - person => new PersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression(int currentYear) => + person => new global::AlephMapper.Tests.MultiParameterInlining.PersonDto { FullName = person.First + " " + person.Last, BirthYear = currentYear - person.Age }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static PersonDto ToDto(Person person, int currentYear, PersonDto dest) + public static global::AlephMapper.Tests.MultiParameterInlining.PersonDto ToDto(global::AlephMapper.Tests.MultiParameterInlining.Person person, int currentYear, global::AlephMapper.Tests.MultiParameterInlining.PersonDto dest) { if (person == null) return dest; if (dest == null) - dest = new PersonDto(); + dest = new global::AlephMapper.Tests.MultiParameterInlining.PersonDto(); dest.FullName = person.First + " " + person.Last; dest.BirthYear = currentYear - person.Age; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Sources/PersonMapper.cs b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Sources/PersonMapper.cs index d0d397f..2264e61 100644 --- a/tests/AlephMapper.Tests/Files/MultiParameterInlining/Sources/PersonMapper.cs +++ b/tests/AlephMapper.Tests/Files/MultiParameterInlining/Sources/PersonMapper.cs @@ -26,7 +26,7 @@ public class PersonDto /// public static partial class PersonMapper { - [Expressive] + [Projectable] public static PersonDto ToDto(Person person) => new() { FullName = Combine(person.First, person.Last), @@ -52,7 +52,7 @@ public static string Describe(string first, string last, int age) => /// public static partial class NamedArgMapper { - [Expressive] + [Projectable] public static PersonDto ToDto(Person person) => new() { FullName = Combine(last: person.Last, first: person.First) @@ -67,7 +67,7 @@ public static partial class NamedArgMapper /// public static partial class NestedMultiParamMapper { - [Expressive] + [Projectable] public static PersonDto ToDto(Person person) => new() { FullName = DescribeWithAge(person.First, person.Last, person.Age) @@ -81,12 +81,12 @@ public static string DescribeWithAge(string first, string last, int age) => /// -/// Tests that a multi-parameter [Expressive] mapping method itself +/// Tests that a multi-parameter [Projectable] mapping method itself /// generates a single-parameter projection expression factory. /// -public static partial class MultiParamExpressiveMapper +public static partial class MultiParamProjectableMapper { - [Expressive] + [Projectable] public static PersonDto Map(Person person, int currentYear) => new() { FullName = person.First + " " + person.Last, @@ -100,7 +100,7 @@ public static partial class MultiParamExpressiveMapper /// public static partial class UpdatableMultiParamMapper { - [Expressive] + [Projectable] [Updatable] public static PersonDto ToDto(Person person, int currentYear) => new() { diff --git a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_AddressMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_AddressMapper_GeneratedMappings.g.cs index 966394c..b8a94e4 100644 --- a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_AddressMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_AddressMapper_GeneratedMappings.g.cs @@ -1,26 +1,29 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NestedExt_AddressMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - a => new NestedExt_AddressDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + a => new global::AlephMapper.Tests.NestedExt_AddressDto { Street = a.Street, City = a.City }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Ignore_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Ignore_GeneratedMappings.g.cs index 14cbaca..82a085a 100644 --- a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Ignore_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Ignore_GeneratedMappings.g.cs @@ -1,29 +1,32 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NestedExt_PersonMapper_Ignore { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - p => new NestedExt_PersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + p => new global::AlephMapper.Tests.NestedExt_PersonDto { - HomeAddress = new NestedExt_AddressDto + HomeAddress = new global::AlephMapper.Tests.NestedExt_AddressDto { Street = p.Friend.HomeAddress.Street, City = p.Friend.HomeAddress.City } }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Rewrite_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Rewrite_GeneratedMappings.g.cs index a6a1f17..37fe5fb 100644 --- a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Rewrite_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Expected/AlephMapper_Tests_NestedExt_PersonMapper_Rewrite_GeneratedMappings.g.cs @@ -1,35 +1,38 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NestedExt_PersonMapper_Rewrite { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> ToDtoExpression() => - p => new NestedExt_PersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + p => new global::AlephMapper.Tests.NestedExt_PersonDto { HomeAddress = (p != null ? ((p.Friend != null ? ((p.Friend.HomeAddress != null - ? (new NestedExt_AddressDto + ? (new global::AlephMapper.Tests.NestedExt_AddressDto { Street = p.Friend.HomeAddress.Street, City = p.Friend.HomeAddress.City }) - : (NestedExt_AddressDto)null)) - : (NestedExt_AddressDto)null)) - : (NestedExt_AddressDto)null) + : (global::AlephMapper.Tests.NestedExt_AddressDto)null)) + : (global::AlephMapper.Tests.NestedExt_AddressDto)null)) + : (global::AlephMapper.Tests.NestedExt_AddressDto)null) }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Sources/NestedConditionalExtensionInlining.cs b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Sources/NestedConditionalExtensionInlining.cs index 950046e..9e31b65 100644 --- a/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Sources/NestedConditionalExtensionInlining.cs +++ b/tests/AlephMapper.Tests/Files/NestedConditionalExtensionInlining/Sources/NestedConditionalExtensionInlining.cs @@ -27,7 +27,7 @@ public class NestedExt_PersonDto public static partial class NestedExt_AddressMapper { - [Expressive] + [Projectable] public static NestedExt_AddressDto ToDto(this NestedExt_Address a) => new() { Street = a.Street, @@ -37,20 +37,20 @@ public static partial class NestedExt_AddressMapper // Mapper using nested conditional access chains -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static partial class NestedExt_PersonMapper_Ignore { - [Expressive] + [Projectable] public static NestedExt_PersonDto ToDto(NestedExt_Person p) => new() { HomeAddress = p?.Friend?.HomeAddress?.ToDto() }; } -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class NestedExt_PersonMapper_Rewrite { - [Expressive] + [Projectable] public static NestedExt_PersonDto ToDto(NestedExt_Person p) => new() { HomeAddress = p?.Friend?.HomeAddress?.ToDto() diff --git a/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper_Tests_NullCoalesceValueAssignmentMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper_Tests_NullCoalesceValueAssignmentMapper_GeneratedMappings.g.cs index aa2cd08..cc91a65 100644 --- a/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper_Tests_NullCoalesceValueAssignmentMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Expected/AlephMapper_Tests_NullCoalesceValueAssignmentMapper_GeneratedMappings.g.cs @@ -1,26 +1,29 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NullCoalesceValueAssignmentMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapExpression() => - s => new ValueDest + public static global::System.Linq.Expressions.Expression> MapExpression() => + s => new global::AlephMapper.Tests.ValueDest { Must = s.Maybe ?? 42 }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Sources/IgnoreNullCoalesceValueAssignment.cs b/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Sources/IgnoreNullCoalesceValueAssignment.cs index a77480a..8a9cba0 100644 --- a/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Sources/IgnoreNullCoalesceValueAssignment.cs +++ b/tests/AlephMapper.Tests/Files/NullCoalesceValueAssignment/Sources/IgnoreNullCoalesceValueAssignment.cs @@ -17,7 +17,7 @@ public static partial class NullCoalesceValueAssignmentMapper { // In Ignore mode the null-conditional operator is dropped, but the coalesce (??) // must be preserved so the assignment to a non-nullable value property still compiles. - [Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] + [Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static ValueDest Map(ValueSource s) => new() { Must = s?.Maybe ?? 42 diff --git a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_IgnoreMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_IgnoreMapper_GeneratedMappings.g.cs index f3a3027..27ed001 100644 --- a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_IgnoreMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_IgnoreMapper_GeneratedMappings.g.cs @@ -1,32 +1,37 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class IgnoreMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> GetAddressExpression() => + public static global::System.Linq.Expressions.Expression> GetAddressExpression() => source => source.BirthInfo.Address ?? "Unknown"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> HasAddressExpression() => + public static global::System.Linq.Expressions.Expression> HasAddressExpression() => source => source.BirthInfo.Address != null; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_NoneMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_NoneMapper_GeneratedMappings.g.cs index 70253a6..f62206c 100644 --- a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_NoneMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_NoneMapper_GeneratedMappings.g.cs @@ -1,21 +1,24 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NoneMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are preserved as-is in the expression tree. /// /// - public static Expression> GetNameExpression() => + public static global::System.Linq.Expressions.Expression> GetNameExpression() => source => source.Name; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_RewriteMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_RewriteMapper_GeneratedMappings.g.cs index 2a2a053..764589d 100644 --- a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_RewriteMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Expected/AlephMapper_Tests_RewriteMapper_GeneratedMappings.g.cs @@ -1,36 +1,41 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class RewriteMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetAddressExpression() => + public static global::System.Linq.Expressions.Expression> GetAddressExpression() => dto => (dto.BirthInfo != null ? (dto.BirthInfo.Address) : (string)null) ?? "Unknown"; +#nullable restore +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> HasAddressExpression() => + public static global::System.Linq.Expressions.Expression> HasAddressExpression() => source => (source.BirthInfo != null ? (source.BirthInfo.Address) : (string)null) != null; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Sources/Mappers.cs b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Sources/Mappers.cs index f767316..5989333 100644 --- a/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Sources/Mappers.cs +++ b/tests/AlephMapper.Tests/Files/NullConditionalRewrite/Sources/Mappers.cs @@ -1,9 +1,7 @@ -using System.CodeDom.Compiler; - namespace AlephMapper.Tests; // Test mapper with Ignore policy (now default, but being explicit) -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static partial class IgnoreMapper { public static string GetAddress(SourceDto source) => source.BirthInfo?.Address ?? "Unknown"; @@ -12,7 +10,7 @@ public static partial class IgnoreMapper } // Test mapper with Rewrite policy -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class RewriteMapper { public static string GetAddress(SourceDto dto) => dto.BirthInfo?.Address ?? "Unknown"; @@ -21,9 +19,9 @@ public static partial class RewriteMapper } // Test mapper with None policy (should fail with null conditional operators) -[Expressive(NullConditionalRewrite = NullConditionalRewrite.None)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.None)] public static partial class NoneMapper { // This method should work because it doesn't use null conditional operators public static string GetName(SourceDto source) => source.Name; -} \ No newline at end of file +} diff --git a/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/Tests_NullableDisabledMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/Tests_NullableDisabledMapper_GeneratedMappings.g.cs index df60f3f..971db9a 100644 --- a/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/Tests_NullableDisabledMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NullableDisabled/Expected/Tests_NullableDisabledMapper_GeneratedMappings.g.cs @@ -1,24 +1,27 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NullableDisabledMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetNameExpression() => + public static global::System.Linq.Expressions.Expression> GetNameExpression() => person => (person != null ? (person.Name) : (string)null); +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NullableDisabled/Sources/Source.cs b/tests/AlephMapper.Tests/Files/NullableDisabled/Sources/Source.cs index e02e48e..ccf190a 100644 --- a/tests/AlephMapper.Tests/Files/NullableDisabled/Sources/Source.cs +++ b/tests/AlephMapper.Tests/Files/NullableDisabled/Sources/Source.cs @@ -3,7 +3,7 @@ namespace Tests; -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class NullableDisabledMapper { public static string GetName(Person person) => person?.Name; diff --git a/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/Tests_NullableEnabledMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/Tests_NullableEnabledMapper_GeneratedMappings.g.cs index 44c8fe2..e0aaf98 100644 --- a/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/Tests_NullableEnabledMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/NullableEnabled/Expected/Tests_NullableEnabledMapper_GeneratedMappings.g.cs @@ -1,24 +1,27 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NullableEnabledMapper { +#nullable enable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> GetNameExpression() => + public static global::System.Linq.Expressions.Expression> GetNameExpression() => person => (person != null ? (person.Name) : (string?)null); +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/NullableEnabled/Sources/Source.cs b/tests/AlephMapper.Tests/Files/NullableEnabled/Sources/Source.cs index a738ba9..be49a48 100644 --- a/tests/AlephMapper.Tests/Files/NullableEnabled/Sources/Source.cs +++ b/tests/AlephMapper.Tests/Files/NullableEnabled/Sources/Source.cs @@ -3,7 +3,7 @@ namespace Tests; -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class NullableEnabledMapper { public static string? GetName(Person person) => person?.Name; diff --git a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtAddressMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtAddressMapper_GeneratedMappings.g.cs index 83c965d..85cf493 100644 --- a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtAddressMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtAddressMapper_GeneratedMappings.g.cs @@ -1,22 +1,25 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class TechDebtAddressMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> FormatAddressExpression() => + public static global::System.Linq.Expressions.Expression> FormatAddressExpression() => address => address.FormattedAddress; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperIgnore_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperIgnore_GeneratedMappings.g.cs index 957a5e1..886acd3 100644 --- a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperIgnore_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperIgnore_GeneratedMappings.g.cs @@ -1,26 +1,29 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class TechDebtPersonMapperIgnore { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> ToDtoExpression() => - person => new TechDebtTestPersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.TechDebtTestPersonDto { Name = person.Name, AddressStr = person.Address.FormattedAddress ?? "No Address" }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperNone_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperNone_GeneratedMappings.g.cs index d0d7347..d689bd1 100644 --- a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperNone_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperNone_GeneratedMappings.g.cs @@ -1,12 +1,13 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class TechDebtPersonMapperNone { } diff --git a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperRewrite_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperRewrite_GeneratedMappings.g.cs index f572d17..7925d0b 100644 --- a/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperRewrite_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/TechDebtFix/Expected/AlephMapper_Tests_TechDebtPersonMapperRewrite_GeneratedMappings.g.cs @@ -1,28 +1,31 @@ -using AgileObjects.ReadableExpressions; +// + +using AgileObjects.ReadableExpressions; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class TechDebtPersonMapperRewrite { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are rewritten as explicit null checks for better compatibility. /// /// - public static Expression> ToDtoExpression() => - person => new TechDebtTestPersonDto + public static global::System.Linq.Expressions.Expression> ToDtoExpression() => + person => new global::AlephMapper.Tests.TechDebtTestPersonDto { Name = person.Name, AddressStr = (person.Address != null ? (person.Address.FormattedAddress) : (string)null) ?? "No Address" }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/TechDebtFix/Sources/TechDebtFix.cs b/tests/AlephMapper.Tests/Files/TechDebtFix/Sources/TechDebtFix.cs index 67ee3bc..d9760dd 100644 --- a/tests/AlephMapper.Tests/Files/TechDebtFix/Sources/TechDebtFix.cs +++ b/tests/AlephMapper.Tests/Files/TechDebtFix/Sources/TechDebtFix.cs @@ -29,16 +29,16 @@ public class TechDebtTestPersonDto // Extension method mapper that should trigger the tech debt public static partial class TechDebtAddressMapper { - [Expressive] + [Projectable] public static string FormatAddress(this TechDebtTestAddress address) => address.FormattedAddress; } // Mapper with nested conditional access that triggers ParseExpression tech debt -[Expressive(NullConditionalRewrite = NullConditionalRewrite.None)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.None)] public static partial class TechDebtPersonMapperNone { - [Expressive] + [Projectable] public static TechDebtTestPersonDto ToDto(TechDebtTestPerson person) => new() { Name = person.Name, @@ -47,10 +47,10 @@ public static partial class TechDebtPersonMapperNone }; } -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static partial class TechDebtPersonMapperRewrite { - [Expressive] + [Projectable] public static TechDebtTestPersonDto ToDto(TechDebtTestPerson person) => new() { Name = person.Name, @@ -61,13 +61,13 @@ public static partial class TechDebtPersonMapperRewrite }; } -[Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] +[Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static partial class TechDebtPersonMapperIgnore { - [Expressive] + [Projectable] public static TechDebtTestPersonDto ToDto(TechDebtTestPerson person) => new() { Name = person.Name, AddressStr = person.Address?.FormatAddress() ?? "No Address" }; -} \ No newline at end of file +} diff --git a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_Address1Mapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_Address1Mapper_GeneratedMappings.g.cs index b139120..39d8242 100644 --- a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_Address1Mapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_Address1Mapper_GeneratedMappings.g.cs @@ -1,37 +1,40 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class Address1Mapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDtoExpression() => - sourceAddress => new Address1Dto + public static global::System.Linq.Expressions.Expression> MapToDtoExpression() => + sourceAddress => new global::AlephMapper.Tests.Address1Dto { Line1 = sourceAddress.Line1 == null ? null - : new AddressLineDto + : new global::AlephMapper.Tests.AddressLineDto { Street = sourceAddress.Line1.Street, HouseNumber = sourceAddress.Line1.HouseNumber }, Line2 = sourceAddress.Line2 == null ? null - : new AddressLineDto + : new global::AlephMapper.Tests.AddressLineDto { Street = sourceAddress.Line2.Street, HouseNumber = sourceAddress.Line2.HouseNumber } }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_AddressLineMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_AddressLineMapper_GeneratedMappings.g.cs index 6b214d0..a0c5b52 100644 --- a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_AddressLineMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_AddressLineMapper_GeneratedMappings.g.cs @@ -1,27 +1,30 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class AddressLineMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDtoExpression() => + public static global::System.Linq.Expressions.Expression> MapToDtoExpression() => sourceAddressLine1 => sourceAddressLine1 == null ? null - : new AddressLineDto + : new global::AlephMapper.Tests.AddressLineDto { Street = sourceAddressLine1.Street, HouseNumber = sourceAddressLine1.HouseNumber }; +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_TestModel1Mapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_TestModel1Mapper_GeneratedMappings.g.cs index 26062bb..9d84d62 100644 --- a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_TestModel1Mapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Expected/AlephMapper_Tests_TestModel1Mapper_GeneratedMappings.g.cs @@ -1,39 +1,41 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class TestModel1Mapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToTestModel1DtoExpression() => - source => new TestModel1Dto + public static global::System.Linq.Expressions.Expression> MapToTestModel1DtoExpression() => + source => new global::AlephMapper.Tests.TestModel1Dto { Name = source.Name, SurName = source.SurName, Address = source.Address != null - ? new Address1Dto + ? new global::AlephMapper.Tests.Address1Dto { Line1 = source.Address.Line1 == null ? null - : new AddressLineDto + : new global::AlephMapper.Tests.AddressLineDto { Street = source.Address.Line1.Street, HouseNumber = source.Address.Line1.HouseNumber }, Line2 = source.Address.Line2 == null ? null - : new AddressLineDto + : new global::AlephMapper.Tests.AddressLineDto { Street = source.Address.Line2.Street, HouseNumber = source.Address.Line2.HouseNumber @@ -41,24 +43,26 @@ public static Expression> MapToTestModel1DtoExpr } : null }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static TestModel1Dto MapToTestModel1Dto(TestModel1 source, TestModel1Dto dest) + public static global::AlephMapper.Tests.TestModel1Dto MapToTestModel1Dto(global::AlephMapper.Tests.TestModel1 source, global::AlephMapper.Tests.TestModel1Dto dest) { if (source == null) return dest; if (dest == null) - dest = new TestModel1Dto(); + dest = new global::AlephMapper.Tests.TestModel1Dto(); dest.Name = source.Name; dest.SurName = source.SurName; if (source.Address != null) { if (dest.Address == null) - dest.Address = new Address1Dto(); + dest.Address = new global::AlephMapper.Tests.Address1Dto(); if (source.Address.Line1== null) { dest.Address.Line1 = null; @@ -66,7 +70,7 @@ public static TestModel1Dto MapToTestModel1Dto(TestModel1 source, TestModel1Dto else { if (dest.Address.Line1 == null) - dest.Address.Line1 = new AddressLineDto(); + dest.Address.Line1 = new global::AlephMapper.Tests.AddressLineDto(); dest.Address.Line1.Street = source.Address.Line1.Street; dest.Address.Line1.HouseNumber = source.Address.Line1.HouseNumber; } @@ -77,7 +81,7 @@ public static TestModel1Dto MapToTestModel1Dto(TestModel1 source, TestModel1Dto else { if (dest.Address.Line2 == null) - dest.Address.Line2 = new AddressLineDto(); + dest.Address.Line2 = new global::AlephMapper.Tests.AddressLineDto(); dest.Address.Line2.Street = source.Address.Line2.Street; dest.Address.Line2.HouseNumber = source.Address.Line2.HouseNumber; } @@ -88,4 +92,5 @@ public static TestModel1Dto MapToTestModel1Dto(TestModel1 source, TestModel1Dto } return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Sources/TestModel1.cs b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Sources/TestModel1.cs index a924efb..0cfc234 100644 --- a/tests/AlephMapper.Tests/Files/TestModel1Mapper/Sources/TestModel1.cs +++ b/tests/AlephMapper.Tests/Files/TestModel1Mapper/Sources/TestModel1.cs @@ -1,4 +1,4 @@ -namespace AlephMapper.Tests; +namespace AlephMapper.Tests; internal class TestModel1 { @@ -42,7 +42,7 @@ internal class AddressLineDto } -[Expressive] +[Projectable] internal static partial class TestModel1Mapper { [Updatable] @@ -55,7 +55,7 @@ public static TestModel1Dto MapToTestModel1Dto(TestModel1 source) }; } -[Expressive] +[Projectable] internal static partial class Address1Mapper { public static Address1Dto MapToDto(Address1 sourceAddress) @@ -66,7 +66,7 @@ public static Address1Dto MapToDto(Address1 sourceAddress) }; } -[Expressive] +[Projectable] internal static partial class AddressLineMapper { public static AddressLineDto? MapToDto(AddressLine? sourceAddressLine1) diff --git a/tests/AlephMapper.Tests/Files/Updatable/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/Updatable/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/Updatable/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/Updatable/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/Updatable/Expected/Tests_SampleMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/Updatable/Expected/Tests_SampleMapper_GeneratedMappings.g.cs index 37f6eff..535e387 100644 --- a/tests/AlephMapper.Tests/Files/Updatable/Expected/Tests_SampleMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/Updatable/Expected/Tests_SampleMapper_GeneratedMappings.g.cs @@ -1,27 +1,30 @@ -using AlephMapper; +// + +using AlephMapper; using System; -using System.CodeDom.Compiler; using System.Linq; using System.Linq.Expressions; namespace Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class SampleMapper { +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static Destination Map(Source source, Destination dest) + public static global::Tests.Destination Map(global::Tests.Source source, global::Tests.Destination dest) { if (source == null) return dest; if (dest == null) - dest = new Destination(); + dest = new global::Tests.Destination(); dest.Name = source.Name; dest.Age = source.Age; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper.Attributes.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper.Attributes.g.cs index 5e01a87..b810543 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper.Attributes.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper.Attributes.g.cs @@ -1,11 +1,17 @@ -using System; +// +#nullable enable + +using System; + +using Microsoft.CodeAnalysis; namespace AlephMapper; /// /// Configures how null-conditional operators are handled /// -public enum NullConditionalRewrite +[Embedded] +internal enum NullConditionalRewrite { /// /// Don't rewrite null conditional operators (Default behavior). @@ -31,10 +37,11 @@ public enum NullConditionalRewrite } /// -/// Marks a class to generate expressive companion methods. +/// Marks a class to generate projectable companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class ExpressiveAttribute : Attribute +internal sealed class ProjectableAttribute : Attribute { /// /// Get or set how null-conditional operators are handled @@ -45,8 +52,9 @@ public sealed class ExpressiveAttribute : Attribute /// /// Marks a class to generate update companion methods. /// +[Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class UpdatableAttribute : Attribute +internal sealed class UpdatableAttribute : Attribute { /// /// Gets or sets the policy for handling collection updates during mapping operations @@ -57,8 +65,9 @@ public sealed class UpdatableAttribute : Attribute /// /// Defines which adapted companions are generated for an declaration. /// +[Embedded] [Flags] -public enum AdaptGeneration +internal enum AdaptGeneration { /// /// Generate a regular mapping method. @@ -79,8 +88,9 @@ public enum AdaptGeneration /// /// Reuses a mapping method as a compile-time template for one explicitly specified source/destination pair. /// +[Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AdaptAttribute : Attribute +internal sealed class AdaptAttribute : Attribute { /// /// Initializes a new adaptation from the template method to the specified source and destination types. @@ -105,7 +115,7 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// Gets or sets the generated method base name. /// Required when expression generation is requested. /// - public string Name { get; set; } + public string Name { get; set; } = string.Empty; /// /// Gets or sets which adapted companions are generated. @@ -121,7 +131,8 @@ public AdaptAttribute(Type sourceType, Type destinationType) /// /// Defines the policy for handling collection updates during mapping operations /// -public enum CollectionPropertiesPolicy +[Embedded] +internal enum CollectionPropertiesPolicy { /// /// Skip collection updates - collections will not be modified during mapping diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexPropertyMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexPropertyMapper_GeneratedMappings.g.cs index bd7c329..59bc4c0 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexPropertyMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexPropertyMapper_GeneratedMappings.g.cs @@ -1,50 +1,55 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ComplexPropertyMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapExpression() => - source => new ComplexPropertyTestDestination + public static global::System.Linq.Expressions.Expression> MapExpression() => + source => new global::AlephMapper.Tests.ComplexPropertyTestDestination { - NestedClass = new NestedReferenceType + NestedClass = new global::AlephMapper.Tests.NestedReferenceType { InnerValue = source.NestedStruct.InnerValue, - InnerClass = new DeeplyNestedReferenceType + InnerClass = new global::AlephMapper.Tests.DeeplyNestedReferenceType { DeepValue = source.NestedStruct.InnerStruct.DeepValue } } }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static ComplexPropertyTestDestination Map(ComplexPropertyTestSource source, ComplexPropertyTestDestination dest) + public static global::AlephMapper.Tests.ComplexPropertyTestDestination Map(global::AlephMapper.Tests.ComplexPropertyTestSource source, global::AlephMapper.Tests.ComplexPropertyTestDestination dest) { if (dest == null) - dest = new ComplexPropertyTestDestination(); + dest = new global::AlephMapper.Tests.ComplexPropertyTestDestination(); if (dest.NestedClass == null) - dest.NestedClass = new NestedReferenceType(); + dest.NestedClass = new global::AlephMapper.Tests.NestedReferenceType(); dest.NestedClass.InnerValue = source.NestedStruct.InnerValue; if (dest.NestedClass.InnerClass == null) - dest.NestedClass.InnerClass = new DeeplyNestedReferenceType(); + dest.NestedClass.InnerClass = new global::AlephMapper.Tests.DeeplyNestedReferenceType(); dest.NestedClass.InnerClass.DeepValue = source.NestedStruct.InnerStruct.DeepValue; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexValueTypeMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexValueTypeMapper_GeneratedMappings.g.cs index 41f08e5..44ab02e 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexValueTypeMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ComplexValueTypeMapper_GeneratedMappings.g.cs @@ -1,56 +1,58 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ComplexValueTypeMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestinationExpression() => - source => new ComplexValueTypeDestination + public static global::System.Linq.Expressions.Expression> MapToDestinationExpression() => + source => new global::AlephMapper.Tests.ComplexValueTypeDestination { Id = source.Id, Name = source.Name, - Address = new AddressStruct + Address = new global::AlephMapper.Tests.AddressStruct { Street = source.Address.Street, City = source.Address.City, ZipCode = source.Address.ZipCode, - Coordinates = new CoordinatesStruct + Coordinates = new global::AlephMapper.Tests.CoordinatesStruct { Latitude = source.Address.Coordinates.Latitude, Longitude = source.Address.Coordinates.Longitude, Elevation = source.Address.Coordinates.Elevation } }, - ContactInfo = new ContactInfoStruct + ContactInfo = new global::AlephMapper.Tests.ContactInfoStruct { Email = source.ContactInfo.Email, Phone = source.ContactInfo.Phone, PreferredMethod = source.ContactInfo.PreferredMethod, - EmergencyContact = new EmergencyContactStruct + EmergencyContact = new global::AlephMapper.Tests.EmergencyContactStruct { Name = source.ContactInfo.EmergencyContact.Name, Relationship = source.ContactInfo.EmergencyContact.Relationship, Phone = source.ContactInfo.EmergencyContact.Phone } }, - Metadata = new MetadataStruct + Metadata = new global::AlephMapper.Tests.MetadataStruct { CreatedAt = source.Metadata.CreatedAt, UpdatedAt = source.Metadata.UpdatedAt, Version = source.Metadata.Version, - Tags = new TagsStruct + Tags = new global::AlephMapper.Tests.TagsStruct { Primary = source.Metadata.Tags.Primary, Secondary = source.Metadata.Tags.Secondary, @@ -58,4 +60,5 @@ public static Expression + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class EdgeCaseMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapExpression() => - source => new EdgeCaseReferenceTypeDestination + public static global::System.Linq.Expressions.Expression> MapExpression() => + source => new global::AlephMapper.Tests.EdgeCaseReferenceTypeDestination { SimpleValue = source.SimpleValue, - ComplexValue = new EdgeCaseReferenceTypeClass + ComplexValue = new global::AlephMapper.Tests.EdgeCaseReferenceTypeClass { StructValue = source.ComplexValue.StructValue, - NestedClass = new NestedEdgeCaseClass + NestedClass = new global::AlephMapper.Tests.NestedEdgeCaseClass { Value = source.ComplexValue.NestedStruct.Value } } }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static EdgeCaseReferenceTypeDestination Map(EdgeCaseValueTypeSource source, EdgeCaseReferenceTypeDestination dest) + public static global::AlephMapper.Tests.EdgeCaseReferenceTypeDestination Map(global::AlephMapper.Tests.EdgeCaseValueTypeSource source, global::AlephMapper.Tests.EdgeCaseReferenceTypeDestination dest) { if (dest == null) - dest = new EdgeCaseReferenceTypeDestination(); + dest = new global::AlephMapper.Tests.EdgeCaseReferenceTypeDestination(); dest.SimpleValue = source.SimpleValue; if (dest.ComplexValue == null) - dest.ComplexValue = new EdgeCaseReferenceTypeClass(); + dest.ComplexValue = new global::AlephMapper.Tests.EdgeCaseReferenceTypeClass(); dest.ComplexValue.StructValue = source.ComplexValue.StructValue; if (dest.ComplexValue.NestedClass == null) - dest.ComplexValue.NestedClass = new NestedEdgeCaseClass(); + dest.ComplexValue.NestedClass = new global::AlephMapper.Tests.NestedEdgeCaseClass(); dest.ComplexValue.NestedClass.Value = source.ComplexValue.NestedStruct.Value; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_MixedTypeMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_MixedTypeMapper_GeneratedMappings.g.cs index b09b69d..2585acf 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_MixedTypeMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_MixedTypeMapper_GeneratedMappings.g.cs @@ -1,24 +1,26 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; using TUnit.Core; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class MixedTypeMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestinationExpression() => - source => new MixedTypeDestination + public static global::System.Linq.Expressions.Expression> MapToDestinationExpression() => + source => new global::AlephMapper.Tests.MixedTypeDestination { IntValue = source.IntValue, StringValue = source.StringValue, @@ -27,25 +29,27 @@ public static Expression> MapToDesti DateTimeValue = source.DateTimeValue, NullableIntValue = source.NullableIntValue, ReferenceObject = source.ReferenceObject != null - ? new TestObject + ? new global::AlephMapper.Tests.TestObject { Name = source.ReferenceObject.Name, Value = source.ReferenceObject.Value } : null }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static MixedTypeDestination MapToDestination(MixedTypeSource source, MixedTypeDestination dest) + public static global::AlephMapper.Tests.MixedTypeDestination MapToDestination(global::AlephMapper.Tests.MixedTypeSource source, global::AlephMapper.Tests.MixedTypeDestination dest) { if (source == null) return dest; if (dest == null) - dest = new MixedTypeDestination(); + dest = new global::AlephMapper.Tests.MixedTypeDestination(); dest.IntValue = source.IntValue; dest.StringValue = source.StringValue; dest.BoolValue = source.BoolValue; @@ -55,7 +59,7 @@ public static MixedTypeDestination MapToDestination(MixedTypeSource source, Mixe if (source.ReferenceObject != null) { if (dest.ReferenceObject == null) - dest.ReferenceObject = new TestObject(); + dest.ReferenceObject = new global::AlephMapper.Tests.TestObject(); dest.ReferenceObject.Name = source.ReferenceObject.Name; dest.ReferenceObject.Value = source.ReferenceObject.Value; } @@ -65,4 +69,5 @@ public static MixedTypeDestination MapToDestination(MixedTypeSource source, Mixe } return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_NullableValueTypeMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_NullableValueTypeMapper_GeneratedMappings.g.cs index 6c3d02b..b3c314e 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_NullableValueTypeMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_NullableValueTypeMapper_GeneratedMappings.g.cs @@ -1,43 +1,48 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class NullableValueTypeMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestinationExpression() => - source => new NullableValueTypeDestination + public static global::System.Linq.Expressions.Expression> MapToDestinationExpression() => + source => new global::AlephMapper.Tests.NullableValueTypeDestination { NullableIntProperty = source.NullableIntProperty, NullableBoolProperty = source.NullableBoolProperty, NullableDateTimeProperty = source.NullableDateTimeProperty }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static NullableValueTypeDestination MapToDestination(NullableValueTypeSource source, NullableValueTypeDestination dest) + public static global::AlephMapper.Tests.NullableValueTypeDestination MapToDestination(global::AlephMapper.Tests.NullableValueTypeSource source, global::AlephMapper.Tests.NullableValueTypeDestination dest) { if (source == null) return dest; if (dest == null) - dest = new NullableValueTypeDestination(); + dest = new global::AlephMapper.Tests.NullableValueTypeDestination(); dest.NullableIntProperty = source.NullableIntProperty; dest.NullableBoolProperty = source.NullableBoolProperty; dest.NullableDateTimeProperty = source.NullableDateTimeProperty; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ReferenceTypeOnlyMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ReferenceTypeOnlyMapper_GeneratedMappings.g.cs index dc23cc1..50ccf17 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ReferenceTypeOnlyMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ReferenceTypeOnlyMapper_GeneratedMappings.g.cs @@ -1,49 +1,53 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ReferenceTypeOnlyMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestinationExpression() => - source => new ReferenceTypeOnlyDestination + public static global::System.Linq.Expressions.Expression> MapToDestinationExpression() => + source => new global::AlephMapper.Tests.ReferenceTypeOnlyDestination { StringProperty = source.StringProperty, ObjectProperty = source.ObjectProperty != null - ? new SimpleReferenceObject + ? new global::AlephMapper.Tests.SimpleReferenceObject { Name = source.ObjectProperty.Name } : null }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static ReferenceTypeOnlyDestination MapToDestination(ReferenceTypeOnlySource source, ReferenceTypeOnlyDestination dest) + public static global::AlephMapper.Tests.ReferenceTypeOnlyDestination MapToDestination(global::AlephMapper.Tests.ReferenceTypeOnlySource source, global::AlephMapper.Tests.ReferenceTypeOnlyDestination dest) { if (source == null) return dest; if (dest == null) - dest = new ReferenceTypeOnlyDestination(); + dest = new global::AlephMapper.Tests.ReferenceTypeOnlyDestination(); dest.StringProperty = source.StringProperty; if (source.ObjectProperty != null) { if (dest.ObjectProperty == null) - dest.ObjectProperty = new SimpleReferenceObject(); + dest.ObjectProperty = new global::AlephMapper.Tests.SimpleReferenceObject(); dest.ObjectProperty.Name = source.ObjectProperty.Name; } else @@ -52,4 +56,5 @@ public static ReferenceTypeOnlyDestination MapToDestination(ReferenceTypeOnlySou } return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_SimpleValueToReferenceMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_SimpleValueToReferenceMapper_GeneratedMappings.g.cs index 62966a3..5da7337 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_SimpleValueToReferenceMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_SimpleValueToReferenceMapper_GeneratedMappings.g.cs @@ -1,40 +1,45 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class SimpleValueToReferenceMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapExpression() => - source => new SimpleReferenceTypeDestination + public static global::System.Linq.Expressions.Expression> MapExpression() => + source => new global::AlephMapper.Tests.SimpleReferenceTypeDestination { Value = source.Value, Name = source.Name }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static SimpleReferenceTypeDestination Map(SimpleValueTypeSource source, SimpleReferenceTypeDestination dest) + public static global::AlephMapper.Tests.SimpleReferenceTypeDestination Map(global::AlephMapper.Tests.SimpleValueTypeSource source, global::AlephMapper.Tests.SimpleReferenceTypeDestination dest) { if (dest == null) - dest = new SimpleReferenceTypeDestination(); + dest = new global::AlephMapper.Tests.SimpleReferenceTypeDestination(); dest.Value = source.Value; dest.Name = source.Name; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueToReferenceMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueToReferenceMapper_GeneratedMappings.g.cs index 4ca2235..8c750c0 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueToReferenceMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueToReferenceMapper_GeneratedMappings.g.cs @@ -1,56 +1,58 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ValueToReferenceMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToReferenceDestinationExpression() => - source => new ComplexReferenceTypeDestination + public static global::System.Linq.Expressions.Expression> MapToReferenceDestinationExpression() => + source => new global::AlephMapper.Tests.ComplexReferenceTypeDestination { Id = source.Id, Name = source.Name, - Address = new AddressClass + Address = new global::AlephMapper.Tests.AddressClass { Street = source.Address.Street, City = source.Address.City, ZipCode = source.Address.ZipCode, - Coordinates = new CoordinatesClass + Coordinates = new global::AlephMapper.Tests.CoordinatesClass { Latitude = source.Address.Coordinates.Latitude, Longitude = source.Address.Coordinates.Longitude, Elevation = source.Address.Coordinates.Elevation } }, - ContactInfo = new ContactInfoClass + ContactInfo = new global::AlephMapper.Tests.ContactInfoClass { Email = source.ContactInfo.Email, Phone = source.ContactInfo.Phone, PreferredMethod = source.ContactInfo.PreferredMethod, - EmergencyContact = new EmergencyContactClass + EmergencyContact = new global::AlephMapper.Tests.EmergencyContactClass { Name = source.ContactInfo.EmergencyContact.Name, Relationship = source.ContactInfo.EmergencyContact.Relationship, Phone = source.ContactInfo.EmergencyContact.Phone } }, - Metadata = new MetadataClass + Metadata = new global::AlephMapper.Tests.MetadataClass { CreatedAt = source.Metadata.CreatedAt, UpdatedAt = source.Metadata.UpdatedAt, Version = source.Metadata.Version, - Tags = new TagsClass + Tags = new global::AlephMapper.Tests.TagsClass { Primary = source.Metadata.Tags.Primary, Secondary = source.Metadata.Tags.Secondary, @@ -58,49 +60,52 @@ public static Expression - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static ComplexReferenceTypeDestination MapToReferenceDestination(ComplexValueTypeSource source, ComplexReferenceTypeDestination dest) + public static global::AlephMapper.Tests.ComplexReferenceTypeDestination MapToReferenceDestination(global::AlephMapper.Tests.ComplexValueTypeSource source, global::AlephMapper.Tests.ComplexReferenceTypeDestination dest) { if (dest == null) - dest = new ComplexReferenceTypeDestination(); + dest = new global::AlephMapper.Tests.ComplexReferenceTypeDestination(); dest.Id = source.Id; dest.Name = source.Name; if (dest.Address == null) - dest.Address = new AddressClass(); + dest.Address = new global::AlephMapper.Tests.AddressClass(); dest.Address.Street = source.Address.Street; dest.Address.City = source.Address.City; dest.Address.ZipCode = source.Address.ZipCode; if (dest.Address.Coordinates == null) - dest.Address.Coordinates = new CoordinatesClass(); + dest.Address.Coordinates = new global::AlephMapper.Tests.CoordinatesClass(); dest.Address.Coordinates.Latitude = source.Address.Coordinates.Latitude; dest.Address.Coordinates.Longitude = source.Address.Coordinates.Longitude; dest.Address.Coordinates.Elevation = source.Address.Coordinates.Elevation; if (dest.ContactInfo == null) - dest.ContactInfo = new ContactInfoClass(); + dest.ContactInfo = new global::AlephMapper.Tests.ContactInfoClass(); dest.ContactInfo.Email = source.ContactInfo.Email; dest.ContactInfo.Phone = source.ContactInfo.Phone; dest.ContactInfo.PreferredMethod = source.ContactInfo.PreferredMethod; if (dest.ContactInfo.EmergencyContact == null) - dest.ContactInfo.EmergencyContact = new EmergencyContactClass(); + dest.ContactInfo.EmergencyContact = new global::AlephMapper.Tests.EmergencyContactClass(); dest.ContactInfo.EmergencyContact.Name = source.ContactInfo.EmergencyContact.Name; dest.ContactInfo.EmergencyContact.Relationship = source.ContactInfo.EmergencyContact.Relationship; dest.ContactInfo.EmergencyContact.Phone = source.ContactInfo.EmergencyContact.Phone; if (dest.Metadata == null) - dest.Metadata = new MetadataClass(); + dest.Metadata = new global::AlephMapper.Tests.MetadataClass(); dest.Metadata.CreatedAt = source.Metadata.CreatedAt; dest.Metadata.UpdatedAt = source.Metadata.UpdatedAt; dest.Metadata.Version = source.Metadata.Version; if (dest.Metadata.Tags == null) - dest.Metadata.Tags = new TagsClass(); + dest.Metadata.Tags = new global::AlephMapper.Tests.TagsClass(); dest.Metadata.Tags.Primary = source.Metadata.Tags.Primary; dest.Metadata.Tags.Secondary = source.Metadata.Tags.Secondary; dest.Metadata.Tags.Category = source.Metadata.Tags.Category; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeMapper_GeneratedMappings.g.cs index 30a06e5..f821879 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeMapper_GeneratedMappings.g.cs @@ -1,11 +1,12 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ValueTypeMapper { } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeOnlyMapper_GeneratedMappings.g.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeOnlyMapper_GeneratedMappings.g.cs index b7b9edf..a37fe0d 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeOnlyMapper_GeneratedMappings.g.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Expected/AlephMapper_Tests_ValueTypeOnlyMapper_GeneratedMappings.g.cs @@ -1,45 +1,50 @@ -using System; -using System.CodeDom.Compiler; +// + +using System; using System.Linq; using System.Linq.Expressions; namespace AlephMapper.Tests; -[GeneratedCode("AlephMapper", "0.6.2")] +[global::System.CodeDom.Compiler.GeneratedCode("AlephMapper", "0.7.0.0")] partial class ValueTypeOnlyMapper { +#nullable disable /// - /// This is an auto-generated expression companion for . + /// This is an auto-generated expression companion for . /// /// /// /// Null handling strategy: Null-conditional operators are ignored and treated as regular member access. /// /// - public static Expression> MapToDestinationExpression() => - source => new ValueTypeOnlyDestination + public static global::System.Linq.Expressions.Expression> MapToDestinationExpression() => + source => new global::AlephMapper.Tests.ValueTypeOnlyDestination { IntProperty = source.IntProperty, BoolProperty = source.BoolProperty, DateTimeProperty = source.DateTimeProperty, DecimalProperty = source.DecimalProperty }; +#nullable restore +#nullable disable /// - /// This is an auto-generated update method for . + /// This is an auto-generated update method for . /// /// The source object to map values from. If null, no updates are performed. /// The destination object to update. If null, the new instance is created. /// The updated destination object for method chaining, or the new destination instance if either parameter is null. - public static ValueTypeOnlyDestination MapToDestination(ValueTypeOnlySource source, ValueTypeOnlyDestination dest) + public static global::AlephMapper.Tests.ValueTypeOnlyDestination MapToDestination(global::AlephMapper.Tests.ValueTypeOnlySource source, global::AlephMapper.Tests.ValueTypeOnlyDestination dest) { if (source == null) return dest; if (dest == null) - dest = new ValueTypeOnlyDestination(); + dest = new global::AlephMapper.Tests.ValueTypeOnlyDestination(); dest.IntProperty = source.IntProperty; dest.BoolProperty = source.BoolProperty; dest.DateTimeProperty = source.DateTimeProperty; dest.DecimalProperty = source.DecimalProperty; return dest; } +#nullable restore } diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ComplexValueType.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ComplexValueType.cs index a27fd0e..be61c37 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ComplexValueType.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ComplexValueType.cs @@ -1,4 +1,4 @@ -namespace AlephMapper.Tests; +namespace AlephMapper.Tests; // Complex nested value types internal struct ComplexValueTypeSource @@ -78,7 +78,7 @@ internal enum TagCategory Other = 2 } -[Expressive] +[Projectable] internal static partial class ComplexValueTypeMapper { [Updatable] diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/EdgeCases.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/EdgeCases.cs index 89c38f2..c355d24 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/EdgeCases.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/EdgeCases.cs @@ -1,4 +1,4 @@ -namespace AlephMapper.Tests; +namespace AlephMapper.Tests; // Simple test types internal struct SimpleValueTypeSource @@ -13,7 +13,7 @@ internal class SimpleReferenceTypeDestination public string Name { get; set; } } -[Expressive] +[Projectable] internal static partial class SimpleValueToReferenceMapper { [Updatable] @@ -57,7 +57,7 @@ internal class DeeplyNestedReferenceType public string DeepValue { get; set; } } -[Expressive] +[Projectable] internal static partial class ComplexPropertyMapper { [Updatable] @@ -109,7 +109,7 @@ internal class NestedEdgeCaseClass public string Value { get; set; } } -[Expressive] +[Projectable] internal static partial class EdgeCaseMapper { [Updatable] diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/MixedType.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/MixedType.cs index af91ec5..413a363 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/MixedType.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/MixedType.cs @@ -30,7 +30,7 @@ internal class TestObject public int Value { get; set; } } -[Expressive] +[Projectable] internal static partial class MixedTypeMapper { [Updatable] diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueToReferenceType.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueToReferenceType.cs index 60955d6..f1e195c 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueToReferenceType.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueToReferenceType.cs @@ -1,4 +1,4 @@ -namespace AlephMapper.Tests; +namespace AlephMapper.Tests; // Reference type equivalents of the value types internal class ComplexReferenceTypeDestination @@ -55,7 +55,7 @@ internal class TagsClass public TagCategory Category { get; set; } } -[Expressive] +[Projectable] internal static partial class ValueToReferenceMapper { [Updatable] diff --git a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueTypeUpdatableNullChecks.cs b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueTypeUpdatableNullChecks.cs index 9d07ad6..2f64f6e 100644 --- a/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueTypeUpdatableNullChecks.cs +++ b/tests/AlephMapper.Tests/Files/ValueTypesAndReferenceTypes/Sources/ValueTypeUpdatableNullChecks.cs @@ -17,7 +17,7 @@ internal class ValueTypeOnlyDestination public decimal DecimalProperty { get; set; } } -[Expressive] +[Projectable] internal static partial class ValueTypeOnlyMapper { [Updatable] @@ -49,7 +49,7 @@ internal class SimpleReferenceObject public string Name { get; set; } } -[Expressive] +[Projectable] internal static partial class ReferenceTypeOnlyMapper { [Updatable] @@ -79,7 +79,7 @@ internal class NullableValueTypeDestination public DateTime? NullableDateTimeProperty { get; set; } } -[Expressive] +[Projectable] internal static partial class NullableValueTypeMapper { [Updatable] diff --git a/tests/AlephMapper.Tests/SourceGeneratorTests.cs b/tests/AlephMapper.Tests/SourceGeneratorTests.cs index da2573f..d9a06f0 100644 --- a/tests/AlephMapper.Tests/SourceGeneratorTests.cs +++ b/tests/AlephMapper.Tests/SourceGeneratorTests.cs @@ -1,7 +1,13 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Testing; using Microsoft.EntityFrameworkCore; +using AlephMapper.Generation; +using AlephMapper.Diagnostics; +using System.Diagnostics; +using System.Text; namespace AlephMapper.Tests; @@ -17,6 +23,689 @@ public SourceGeneratorTests() _driver = CSharpGeneratorDriver.Create(generators: [generator], parseOptions: _parseOptions); } + [Test] + public async Task UnrelatedMethodsAreRejectedBySyntaxCandidateFilter() + { + var tree = CSharpSyntaxTree.ParseText( + "public class Unrelated { public int Add(int left, int right) => left + right; }", + _parseOptions); + var method = tree.GetRoot().DescendantNodes().OfType().Single(); + + await Assert.That(MappingMethodCandidate.IsCandidate(method, CancellationToken.None)).IsFalse(); + } + + [Test] + public async Task GenerationDiagnosticsPreserveDescriptorMetadata() + { + var compilation = CSharpCompilation.Create("DiagnosticMetadata"); + var original = Diagnostic.Create(DiagnosticDescriptors.GeneratorCrash, Location.None, "boom"); + var roundTripped = GenerationDiagnostic.From(original).ToDiagnostic(compilation); + + await Assert.That(roundTripped.Descriptor.Description.ToString()) + .IsEqualTo(original.Descriptor.Description.ToString()); + await Assert.That(roundTripped.Descriptor.HelpLinkUri) + .IsEqualTo(original.Descriptor.HelpLinkUri); + await Assert.That(DiagnosticDescriptors.GeneratorCrash.HelpLinkUri).Contains("AM0004"); + await Assert.That(DiagnosticDescriptors.GeneratorCrash.HelpLinkUri).DoesNotContain("IMP005"); + } + + [Test] + public async Task DiagnosticRoundTripPreservesFormattedMessage() + { + var compilation = CSharpCompilation.Create("DiagnosticMessage"); + var original = Diagnostic.Create( + DiagnosticDescriptors.AdaptIncompatibleType, + Location.None, + "Map", + "Name"); + var roundTripped = GenerationDiagnostic.From(original).ToDiagnostic(compilation); + + await Assert.That(roundTripped.GetMessage()).IsEqualTo(original.GetMessage()); + } + + [Test] + public async Task GeneratedMembersUseOneTransparentNullablePolicy() + { + var cases = new[] + { + new { Directive = "#nullable disable", Expected = "disable", Annotations = false, ProjectDefault = NullableContextOptions.Disable }, + new { Directive = "#nullable enable", Expected = "enable", Annotations = true, ProjectDefault = NullableContextOptions.Disable }, + new { Directive = "#nullable enable warnings", Expected = "enable warnings", Annotations = false, ProjectDefault = NullableContextOptions.Disable }, + new { Directive = "#nullable enable annotations", Expected = "enable annotations", Annotations = true, ProjectDefault = NullableContextOptions.Disable }, + new { Directive = string.Empty, Expected = "enable", Annotations = true, ProjectDefault = NullableContextOptions.Enable } + }; + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + + foreach (var testCase in cases) + { + var nullableSuffix = testCase.Annotations ? "?" : string.Empty; + var source = $$""" + using AlephMapper; + + namespace NullablePolicyFixture; + + {{testCase.Directive}} + public static partial class Mapper + { + [Projectable] + [Updatable] + [Adapt(typeof(Employee), typeof(EmployeeDto), Name = "MapEmployee")] + public static PersonDto Map(Person source, string{{nullableSuffix}} prefix) => + new() { Name = prefix + source.Name }; + } + {{(testCase.Directive.Length == 0 ? string.Empty : "#nullable restore")}} + + public sealed class Person { public string Name { get; set; } = string.Empty; } + public sealed class Employee { public string Name { get; set; } = string.Empty; } + public sealed class PersonDto { public string Name { get; set; } = string.Empty; } + public sealed class EmployeeDto { public string Name { get; set; } = string.Empty; } + """; + var compilation = CSharpCompilation.Create( + "NullablePolicy_" + testCase.Expected.Replace(" ", "_", StringComparison.Ordinal), + [CSharpSyntaxTree.ParseText(source, _parseOptions)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithNullableContextOptions(testCase.ProjectDefault)); + + var driver = _driver.RunGeneratorsAndUpdateCompilation( + compilation, + out var outputCompilation, + out var generatorDiagnostics); + var result = driver.GetRunResult().Results.Single(); + var mapperSource = result.GeneratedSources + .Select(generated => generated.SourceText.ToString()) + .Single(generated => generated.Contains("partial class Mapper", StringComparison.Ordinal)); + var generatedTrees = outputCompilation.SyntaxTrees + .Where(tree => !compilation.SyntaxTrees.Contains(tree)) + .ToHashSet(); + + await Assert.That(generatorDiagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(result.Diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(outputCompilation.GetDiagnostics().Where(diagnostic => + diagnostic.Severity == DiagnosticSeverity.Error && + diagnostic.Location.SourceTree is { } tree && + generatedTrees.Contains(tree))).IsEmpty(); + await Assert.That(outputCompilation.GetDiagnostics().Where(diagnostic => + diagnostic.Severity == DiagnosticSeverity.Warning && + diagnostic.Id.StartsWith("CS86", StringComparison.Ordinal) && + diagnostic.Location.SourceTree is { } tree && + generatedTrees.Contains(tree))).IsEmpty(); + await Assert.That(mapperSource).Contains("#nullable " + testCase.Expected); + await Assert.That(mapperSource.StartsWith("// " + Environment.NewLine + Environment.NewLine, StringComparison.Ordinal)).IsTrue(); + await Assert.That(mapperSource.Contains("string? prefix", StringComparison.Ordinal)).IsEqualTo(testCase.Annotations); + await Assert.That(mapperSource).Contains("MapExpression"); + await Assert.That(mapperSource).Contains("MapEmployeeExpression"); + await Assert.That(mapperSource).Contains("MapEmployee("); + } + } + + [Test] + public async Task MapperHelpersRemainCandidatesForInlining() + { + var tree = CSharpSyntaxTree.ParseText( + "using AlephMapper; public static partial class Mapper { [Projectable] public static int Map(int value) => Helper(value); public static int Helper(int value) => value; }", + _parseOptions); + var methods = tree.GetRoot().DescendantNodes().OfType().ToArray(); + + await Assert.That(methods).Count().IsEqualTo(2); + await Assert.That(MappingMethodCandidate.IsCandidate(methods.Single(method => method.Identifier.ValueText == "Helper"), CancellationToken.None)).IsTrue(); + } + + [Test] + public async Task ExternalOrdinaryHelpersAreInlinedIntoProjectableMappings() + { + const string source = """ + using AlephMapper; + + namespace Fixture; + + public sealed class Person + { + public string FirstName { get; set; } = ""; + public string LastName { get; set; } = ""; + } + + public sealed class PersonDto + { + public string Name { get; set; } = ""; + } + + public static class ExternalHelpers + { + public static string FullName(Person person) => person.FirstName + " " + person.LastName; + } + + public static partial class PersonMapper + { + [Projectable] + public static PersonDto Map(Person person) => new() { Name = ExternalHelpers.FullName(person) }; + } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var compilation = CSharpCompilation.Create( + "ExternalHelperInlining", + [CSharpSyntaxTree.ParseText(source, _parseOptions)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + var generated = driver.GetRunResult().Results.Single().GeneratedSources + .Single(result => result.HintName.EndsWith("PersonMapper_GeneratedMappings.g.cs", StringComparison.Ordinal)) + .SourceText + .ToString(); + + await Assert.That(diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(generated).DoesNotContain("ExternalHelpers.FullName"); + await Assert.That(generated).Contains("person.FirstName + \" \" + person.LastName"); + } + + [Test] + public async Task ClassLevelConfigurationAcrossPartialDeclarationsGeneratesOnce() + { + const string configuration = """ + using AlephMapper; + namespace Fixture; + [Projectable] + public static partial class Mapper { } + """; + const string mapping = """ + namespace Fixture; + public static partial class Mapper + { + public static Target Map(Source source) => new() { Value = source.Value }; + } + public sealed class Source { public int Value { get; set; } } + public sealed class Target { public int Value { get; set; } } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var compilation = CSharpCompilation.Create( + "PartialMapper", + [ + CSharpSyntaxTree.ParseText(configuration, _parseOptions, "Configuration.cs"), + CSharpSyntaxTree.ParseText(mapping, _parseOptions, "Mapping.cs") + ], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = CSharpGeneratorDriver.Create( + generators: [new AlephSourceGenerator().AsSourceGenerator()], + parseOptions: _parseOptions); + var updatedDriver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + var generatorResult = updatedDriver.GetRunResult().Results.Single(); + + await Assert.That(diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(generatorResult.Exception).IsNull(); + await Assert.That(generatorResult.GeneratedSources + .Count(source => source.HintName.EndsWith("Mapper_GeneratedMappings.g.cs", StringComparison.Ordinal))).IsEqualTo(1); + } + + [Test] + public async Task GeneratedConfigurationTypesAreNotPublic() + { + var assembly = typeof(AlephSourceGenerator).Assembly; + var typeNames = new[] + { + "ProjectableAttribute", + "UpdatableAttribute", + "AdaptAttribute", + "NullConditionalRewrite", + "CollectionPropertiesPolicy", + "AdaptGeneration" + }; + + foreach (var typeName in typeNames) + { + var type = assembly.GetType("AlephMapper." + typeName); + await Assert.That(type).IsNotNull(); + await Assert.That(type!.IsPublic).IsFalse(); + } + + await Assert.That(assembly.GetType("AlephMapper.ExpressiveAttribute")).IsNull(); + } + + [Test] + public async Task GeneratedTypeReferencesAreGloballyQualified() + { + const string source = """ + using AlephMapper; + + namespace Collision; + + public sealed class Func { } + public sealed class Expression { } + public sealed class Source { public string Name { get; set; } = ""; } + public sealed class Destination { public string Name { get; set; } = ""; } + + public static partial class Mapper + { + [Projectable] + [Updatable] + public static Destination Map(Source source) => new() { Name = source.Name }; + } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var compilation = CSharpCompilation.Create( + "GloballyQualifiedTypes", + [CSharpSyntaxTree.ParseText(source, _parseOptions)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + var generated = driver.GetRunResult().Results.Single().GeneratedSources + .Single(result => result.HintName.EndsWith("Mapper_GeneratedMappings.g.cs", StringComparison.Ordinal)) + .SourceText + .ToString(); + + await Assert.That(diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(generated).Contains("global::System.Linq.Expressions.Expression>"); + await Assert.That(generated).Contains("new global::Collision.Destination"); + await Assert.That(generated).Contains("dest = new global::Collision.Destination();"); + } + + [Test] + public async Task EmbeddedConfigurationTypesDoNotConflictAcrossConsumerAssemblies() + { + const string projectASource = """ + using AlephMapper; + [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ProjectB")] + namespace ProjectA; + public static partial class Mapper + { + [Projectable] + public static Target Map(Source source) => new() { Value = source.Value }; + } + public sealed class Source { public int Value { get; set; } } + public sealed class Target { public int Value { get; set; } } + """; + const string projectBSource = """ + using AlephMapper; + using ProjectA; + namespace ProjectB; + public static partial class Mapper + { + [Projectable] + public static Target Map(Source source) => new() { Value = source.Value }; + } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var projectA = CSharpCompilation.Create( + "ProjectA", + [CSharpSyntaxTree.ParseText(projectASource, _parseOptions, "ProjectA.cs")], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var projectADriver = CSharpGeneratorDriver.Create( + generators: [new AlephSourceGenerator().AsSourceGenerator()], + parseOptions: _parseOptions); + projectADriver.RunGeneratorsAndUpdateCompilation(projectA, out var projectAOutput, out var projectADiagnostics); + await Assert.That(projectADiagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(projectAOutput.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + + using var projectAImage = new MemoryStream(); + var emitResult = projectAOutput.Emit(projectAImage); + await Assert.That(emitResult.Success).IsTrue(); + + var projectB = CSharpCompilation.Create( + "ProjectB", + [CSharpSyntaxTree.ParseText(projectBSource, _parseOptions, "ProjectB.cs")], + references.Add(MetadataReference.CreateFromImage(projectAImage.ToArray())), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var projectBDriver = CSharpGeneratorDriver.Create( + generators: [new AlephSourceGenerator().AsSourceGenerator()], + parseOptions: _parseOptions); + projectBDriver.RunGeneratorsAndUpdateCompilation(projectB, out var projectBOutput, out var projectBDiagnostics); + + await Assert.That(projectBDiagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(projectBOutput.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(projectBOutput.GetTypeByMetadataName("AlephMapper.ProjectableAttribute")!.DeclaredAccessibility) + .IsEqualTo(Accessibility.Internal); + await Assert.That(projectBOutput.GetTypeByMetadataName("AlephMapper.ProjectableAttribute")!.GetAttributes() + .Any(attribute => attribute.AttributeClass?.ToDisplayString() == "Microsoft.CodeAnalysis.EmbeddedAttribute")) + .IsTrue(); + } + + [Test] + public async Task AttributeDiscoveryGeneratesOneFileForCombinedConfiguration() + { + const string source = """ + using AlephMapper; + namespace Fixture; + public static partial class Mapper + { + [Projectable] + [Updatable] + [Adapt(typeof(AdaptedSource), typeof(AdaptedTarget), Name = "MapAdapted")] + public static Target Map(Source source) => new() { Value = source.Value }; + } + public sealed class Source { public int Value { get; set; } } + public sealed class Target { public int Value { get; set; } } + public sealed class AdaptedSource { public int Value { get; set; } } + public sealed class AdaptedTarget { public int Value { get; set; } } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var compilation = CSharpCompilation.Create( + "CombinedConfiguration", + [CSharpSyntaxTree.ParseText(source, _parseOptions, "Mapper.cs")], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + var result = driver.GetRunResult().Results.Single(); + + await Assert.That(diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(result.GeneratedSources + .Count(generated => generated.HintName.EndsWith("Mapper_GeneratedMappings.g.cs", StringComparison.Ordinal))) + .IsEqualTo(1); + } + + [Test] + public async Task MapperGenerationResultRemainsStableWhenUnrelatedSourceChanges() + { + const string mapperA = """ + using AlephMapper; + namespace Fixture; + public static partial class MapperA + { + [Projectable] + public static Target Map(Source source) => new() { Value = source.Value }; + } + public sealed class Source { public int Value { get; set; } } + public sealed class Target { public int Value { get; set; } } + """; + const string mapperB = """ + using AlephMapper; + namespace Fixture; + public static partial class MapperB + { + [Projectable] + public static Target Map(Source source) => new() { Value = source.Value }; + } + """; + const string unrelated = "namespace Fixture; public sealed class Unrelated { public int Value => 1; }"; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var mapperATree = CSharpSyntaxTree.ParseText(mapperA, _parseOptions, "MapperA.cs"); + var mapperBTree = CSharpSyntaxTree.ParseText(mapperB, _parseOptions, "MapperB.cs"); + var unrelatedTree = CSharpSyntaxTree.ParseText(unrelated, _parseOptions, "Unrelated.cs"); + var compilation = CSharpCompilation.Create( + "IncrementalTracking", + [mapperATree, mapperBTree, unrelatedTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = CreateTrackingDriver().RunGeneratorsAndUpdateCompilation(compilation, out _, out _); + var updatedUnrelatedTree = unrelatedTree.WithChangedText(SourceText.From( + "namespace Fixture; public sealed class Unrelated { public int Value => 2; }")); + var updatedCompilation = compilation.ReplaceSyntaxTree(unrelatedTree, updatedUnrelatedTree); + driver = driver.RunGeneratorsAndUpdateCompilation(updatedCompilation, out _, out _); + + var trackedSteps = driver.GetRunResult().Results.Single().TrackedSteps; + var sourceOutputs = trackedSteps["AlephMapper.ProjectableSourceOutput"] + .SelectMany(static step => step.Outputs) + .ToArray(); + + await Assert.That(sourceOutputs).Count().IsEqualTo(2); + await Assert.That(sourceOutputs.All(static output => + output.Item2 is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged)).IsTrue(); + } + + [Test] + public async Task SourceOutputRemainsUnchangedWhenOnlyDiagnosticLocationChanges() + { + const string source = """ + using AlephMapper; + + namespace Fixture; + + public interface ISource { string Name { get; } } + public interface IDestination { string Name { get; set; } } + public sealed class AdaptedSource : ISource { public string Name { get; set; } = string.Empty; } + public sealed class AdaptedDestination : IDestination { public string Name { get; set; } = string.Empty; } + + public static partial class Mapper + { + [Projectable] + [Adapt(typeof(AdaptedSource), typeof(AdaptedDestination), Name = "MapAdapted")] + public static TResult Map(TSource source) + where TSource : ISource + where TResult : IDestination, new() => new() { Name = source.Name }; + } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var syntaxTree = CSharpSyntaxTree.ParseText(source, _parseOptions, "Mapper.cs"); + var compilation = CSharpCompilation.Create( + "DiagnosticOnlyChange", + [syntaxTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = CreateTrackingDriver().RunGeneratorsAndUpdateCompilation(compilation, out _, out _); + var initialResult = driver.GetRunResult().Results.Single(); + var generatedSource = initialResult.GeneratedSources + .Single(static generated => generated.HintName.EndsWith("Mapper_GeneratedMappings.g.cs", StringComparison.Ordinal)) + .SourceText + .ToString(); + var initialDiagnostic = initialResult.Diagnostics.Single(static diagnostic => diagnostic.Id == "AM0018"); + var updatedSyntaxTree = syntaxTree.WithChangedText(SourceText.From("\n" + source)); + var updatedCompilation = compilation.ReplaceSyntaxTree(syntaxTree, updatedSyntaxTree); + driver = driver.RunGeneratorsAndUpdateCompilation(updatedCompilation, out _, out _); + + var result = driver.GetRunResult().Results.Single(); + var updatedGeneratedSource = result.GeneratedSources + .Single(static generated => generated.HintName.EndsWith("Mapper_GeneratedMappings.g.cs", StringComparison.Ordinal)) + .SourceText + .ToString(); + var sourceOutputs = result.TrackedSteps["AlephMapper.ProjectableSourceOutput"] + .SelectMany(static step => step.Outputs) + .ToArray(); + var diagnostic = result.Diagnostics.Single(static diagnostic => diagnostic.Id == "AM0018"); + + await Assert.That(updatedGeneratedSource).IsEqualTo(generatedSource); + await Assert.That(diagnostic.Location.GetLineSpan().StartLinePosition.Line) + .IsEqualTo(initialDiagnostic.Location.GetLineSpan().StartLinePosition.Line + 1); + await Assert.That(sourceOutputs).Count().IsEqualTo(1); + await Assert.That(sourceOutputs.Single().Item2).IsEqualTo(IncrementalStepRunReason.Unchanged); + } + + [Test] + public async Task GenericMethodsGenerateProjectableAndUpdatableCompanions() + { + const string source = """ + #nullable enable + using AlephMapper; + + namespace Fixture; + + public interface ISource + { + string Name { get; } + } + + public interface IDestination + { + string Name { get; set; } + } + + public static partial class Mapper + { + [Projectable] + [Updatable] + public static TResult Map(TSource source) + where TSource : ISource + where TResult : IDestination, new() => new() + { + Name = source.Name + }; + } + """; + + var generatedSources = await AssertAdaptedOutputCompiles(source, "GenericMethodMappings"); + var generatedSource = generatedSources.Single(sourceText => sourceText.Contains("MapExpression")); + + await Assert.That(generatedSource).Contains("MapExpression()"); + await Assert.That(generatedSource).Contains("Map(TSource source, TResult dest)"); + await Assert.That(generatedSource).Contains("where TSource : global::Fixture.ISource"); + await Assert.That(generatedSource).Contains("where TResult : global::Fixture.IDestination, new()"); + } + + [Test] + public async Task GenericMethodsDoNotGenerateAdaptedCompanions() + { + const string source = """ + using AlephMapper; + + namespace Fixture; + + public interface ISource + { + string Name { get; } + } + + public interface IDestination + { + string Name { get; set; } + } + + public sealed class AdaptedSource : ISource + { + public string Name { get; set; } = string.Empty; + } + + public sealed class AdaptedDestination : IDestination + { + public string Name { get; set; } = string.Empty; + } + + public static partial class Mapper + { + [Adapt(typeof(AdaptedSource), typeof(AdaptedDestination), Name = "MapAdapted")] + public static TResult Map(TSource source) + where TSource : ISource + where TResult : IDestination, new() => new() + { + Name = source.Name + }; + } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var compilation = CSharpCompilation.Create( + "GenericAdaptation", + [CSharpSyntaxTree.ParseText(source, _parseOptions)], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); + var result = driver.GetRunResult().Results.Single(); + + await Assert.That(result.Diagnostics.Any(diagnostic => diagnostic.Id == "AM0018")).IsTrue(); + await Assert.That(result.GeneratedSources.Any(generated => + generated.SourceText.ToString().Contains("MapAdapted", StringComparison.Ordinal))).IsFalse(); + await Assert.That(outputCompilation.GetDiagnostics() + .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + } + + [Test] + public async Task PragmaCanSuppressGenericAdaptationDiagnostic() + { + const string source = """ + using AlephMapper; + + namespace Fixture; + + public interface ISource { string Name { get; } } + public interface IDestination { string Name { get; set; } } + public sealed class AdaptedSource : ISource { public string Name { get; set; } = string.Empty; } + public sealed class AdaptedDestination : IDestination { public string Name { get; set; } = string.Empty; } + + public static partial class Mapper + { + #pragma warning disable AM0018 + [Adapt(typeof(AdaptedSource), typeof(AdaptedDestination), Name = "MapAdapted")] + public static TResult Map(TSource source) + where TSource : ISource + where TResult : IDestination, new() => new() { Name = source.Name }; + #pragma warning restore AM0018 + } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var syntaxTree = CSharpSyntaxTree.ParseText(source, _parseOptions, "PragmaSuppression.cs"); + var compilation = CSharpCompilation.Create( + "PragmaSuppression", + [syntaxTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out var generatorDiagnostics); + var result = driver.GetRunResult().Results.Single(); + + await Assert.That(generatorDiagnostics.Any(diagnostic => + diagnostic.Id == "AM0018" && !diagnostic.IsSuppressed)).IsFalse(); + await Assert.That(result.Diagnostics.Any(diagnostic => + diagnostic.Id == "AM0018" && !diagnostic.IsSuppressed)).IsFalse(); + } + + [Test] + public async Task LargeCompilationDiscoversOnlyAttributedMappers() + { + var source = new StringBuilder("using AlephMapper; namespace Fixture; public sealed class Source { public int Value { get; set; } } public sealed class Target { public int Value { get; set; } } public sealed class Unrelated {"); + for (var index = 0; index < 1_000; index++) + { + source.Append("public int Method").Append(index).Append("() => ").Append(index).Append(';'); + } + + source.Append('}'); + for (var index = 0; index < 10; index++) + { + source.Append("public static partial class Mapper").Append(index) + .Append(" { [Projectable] public static Target Map(Source source) => new() { Value = source.Value }; }"); + } + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var compilation = CSharpCompilation.Create( + "LargeDiscovery", + [CSharpSyntaxTree.ParseText(source.ToString(), _parseOptions, "Large.cs")], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var stopwatch = Stopwatch.StartNew(); + var driver = CreateTrackingDriver().RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + stopwatch.Stop(); + + var result = driver.GetRunResult().Results.Single(); + var candidateOutputs = result.TrackedSteps["AlephMapper.ProjectableSourceOutput"] + .SelectMany(static step => step.Outputs) + .ToArray(); + + Console.WriteLine($"Large discovery completed in {stopwatch.ElapsedMilliseconds} ms."); + await Assert.That(diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(candidateOutputs).Count().IsEqualTo(10); + await Assert.That(result.GeneratedSources + .Count(generated => generated.HintName.EndsWith("_GeneratedMappings.g.cs", StringComparison.Ordinal))) + .IsEqualTo(10); + } + + private CSharpGeneratorDriver CreateTrackingDriver() + { + return CSharpGeneratorDriver.Create( + generators: [new AlephSourceGenerator().AsSourceGenerator()], + parseOptions: _parseOptions, + driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); + } + public static IEnumerable GetTestCases() { var groupedByTestCase = Nones.GetMatches("Files/**/*.cs") @@ -72,7 +761,11 @@ public async Task GenerationMatchesBaseLine(string name, string[] sourceFiles, s var syntaxTrees = sourceTrees.Append(globalUsings).ToArray(); var references = (await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None)) - .Add(MetadataReference.CreateFromFile(typeof(DbContext).Assembly.Location)); + .Add(MetadataReference.CreateFromFile(typeof(DbContext).Assembly.Location)) + .Add(MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "AgileObjects.NetStandardPolyfills.dll"))) + .Add(MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "AgileObjects.ReadableExpressions.dll"))) + .Add(MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "TUnit.Assertions.dll"))) + .Add(MetadataReference.CreateFromFile(Path.Combine(AppContext.BaseDirectory, "TUnit.Core.dll"))); var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); @@ -82,11 +775,28 @@ public async Task GenerationMatchesBaseLine(string name, string[] sourceFiles, s references, compilationOptions); - var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out var generatorDiagnostics); + var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generatorDiagnostics); await Assert.That(generatorDiagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)).IsEmpty(); - var result = driver.GetRunResult().Results.Single(); + var generatedSyntaxTrees = outputCompilation.SyntaxTrees + .Where(tree => !syntaxTrees.Contains(tree)) + .ToHashSet(); + await Assert.That(outputCompilation.GetDiagnostics() + .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error && + diagnostic.Location.SourceTree is { } sourceTree && + generatedSyntaxTrees.Contains(sourceTree))).IsEmpty(); + // The nullable-disabled fixture specifically verifies that generated output preserves + // the source nullable context without introducing nullable-flow warnings. Other + // fixtures intentionally exercise policies that dereference nullable values. + if (name == "NullableDisabled") + { + await Assert.That(outputCompilation.GetDiagnostics() + .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Warning && + diagnostic.Id.StartsWith("CS86", StringComparison.Ordinal) && + diagnostic.Location.SourceTree is { } sourceTree && + generatedSyntaxTrees.Contains(sourceTree))).IsEmpty(); + } var actualSources = result.GeneratedSources.ToDictionary( source => Path.GetFileName(source.HintName), @@ -342,8 +1052,8 @@ public sealed class EmployeeTaxInfo { public decimal Rate { get; set; } } await Assert.That(generatedSource).DoesNotContain("?."); await Assert.That(generatedSource).DoesNotContain("MapNonNullPerson"); await Assert.That(generatedSource).Contains("source != null"); - await Assert.That(generatedSource).Contains("new EmployeeDto"); - await Assert.That(generatedSource).Contains("Tax = new EmployeeTaxInfo"); + await Assert.That(generatedSource).Contains("new global::Fixture.EmployeeDto"); + await Assert.That(generatedSource).Contains("Tax = new global::Fixture.EmployeeTaxInfo"); } [Test] @@ -455,8 +1165,8 @@ public sealed class ReadOnlyTaxInfo await Assert.That(generatedSource).DoesNotContain("?."); await Assert.That(generatedSource).DoesNotContain("MapNonNullInputToItem"); await Assert.That(generatedSource).Contains("source != null"); - await Assert.That(generatedSource).Contains("new ReadOnlyOrderItem"); - await Assert.That(generatedSource).Contains("Tax = new ReadOnlyTaxInfo"); + await Assert.That(generatedSource).Contains("new global::Fixture.ReadOnlyOrderItem"); + await Assert.That(generatedSource).Contains("Tax = new global::Fixture.ReadOnlyTaxInfo"); await Assert.That(generatedSource).Contains("TotalAmount = decimal.Round((source.Subtotal * (1m + source.TaxRate / 100m)), 2)"); } @@ -512,7 +1222,7 @@ public sealed class Description { public string Language { get; set; } = string. var generatedSource = generatedSources.Single(sourceText => sourceText.Contains("MapEmployeeExpression")); await Assert.That(generatedSource).DoesNotContain("new string"); - await Assert.That(generatedSource).Contains("new DescriptionWithOrder"); + await Assert.That(generatedSource).Contains("new global::Fixture.Mapper.DescriptionWithOrder"); } [Test] @@ -574,13 +1284,13 @@ namespace Fixture; public static partial class Criteria { - [Expressive] + [Projectable] public static bool HasCategoryLine(InvoiceLine line, Guid dataSetId, Guid invoiceId) => line.Invoice.DataSetId == dataSetId && line.Invoice.InvoiceIdReference == invoiceId && line.CategoryId != null; - [Expressive] + [Projectable] public static bool HasCategoryLineSingleLine(InvoiceLine line, Guid dataSetId, Guid invoiceId) => line.Invoice.DataSetId == dataSetId && line.Invoice.InvoiceIdReference == invoiceId && line.CategoryId != null; } @@ -607,6 +1317,49 @@ public sealed class Invoice await Assert.That(generatedSource).Contains("line => line.Invoice.DataSetId == dataSetId && line.Invoice.InvoiceIdReference == invoiceId && line.CategoryId != null;"); } + [Test] + public async Task ExpressionOutputPreservesConditionalLayoutIncludingLoweredSwitches() + { + const string source = """ + using AlephMapper; + + namespace Fixture; + + public static partial class Mapper + { + [Projectable] + public static string SingleConditional(bool condition) => condition ? "yes" : "no"; + + [Projectable] + public static string MultilineConditional(bool condition) => + condition + ? "yes" + : "no"; + + [Projectable] + public static string SingleSwitch(int value) => value switch { 1 => "one", _ => "other" }; + + [Projectable] + public static string MultilineSwitch(int value) => + value switch + { + 1 => "one", + _ => "other" + }; + } + """; + + var generatedSources = await AssertAdaptedOutputCompiles(source, "ConditionalExpressionFormatting"); + var generatedSource = generatedSources.Single(sourceText => sourceText.Contains("SingleConditionalExpression")); + + await Assert.That(generatedSource).Contains("condition => condition ? \"yes\" : \"no\";"); + await Assert.That(generatedSource).Contains( + $"condition => condition{Environment.NewLine} ? \"yes\"{Environment.NewLine} : \"no\";"); + await Assert.That(generatedSource).Contains("value => value == 1 ? \"one\" : \"other\";"); + await Assert.That(generatedSource).Contains( + $"value => value == 1{Environment.NewLine} ? \"one\"{Environment.NewLine} : \"other\";"); + } + [Test] public async Task AdaptReportsIncompatibleDirectMemberAssignment() { @@ -663,6 +1416,45 @@ public async Task AdaptationDiagnosticsAreReported() } } + [Test] + public async Task AdaptationDiagnosticsPreserveSourceLocation() + { + const string source = """ + using AlephMapper; + + namespace Fixture; + + public static partial class Mapper + { + // Keep the attribute away from the first line to verify line mapping. + [Adapt(typeof(Employee), typeof(EmployeeDto), Name = "MapEmployee")] + public static PersonDto MapPerson(Person source) => new() { Name = source.Name }; + } + + public sealed class Person { public string Name { get; set; } = string.Empty; } + public sealed class Employee { public int Name { get; set; } } + public sealed class PersonDto { public string Name { get; set; } = string.Empty; } + public sealed class EmployeeDto { public string Name { get; set; } = string.Empty; } + """; + + var references = await ReferenceAssemblies.Net.Net90.ResolveAsync(LanguageNames.CSharp, CancellationToken.None); + var syntaxTree = CSharpSyntaxTree.ParseText(source, _parseOptions, "AdaptLocation.cs"); + var compilation = CSharpCompilation.Create( + "AdaptationDiagnosticLocation", + [syntaxTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = _driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _); + var diagnostic = driver.GetRunResult().Results.Single().Diagnostics.Single(diagnostic => diagnostic.Id == "AM0008"); + var lineSpan = diagnostic.Location.GetLineSpan(); + + await Assert.That(diagnostic.Location).IsNotEqualTo(Location.None); + await Assert.That(diagnostic.Location.SourceTree).IsEqualTo(syntaxTree); + await Assert.That(lineSpan.Path).IsEqualTo("AdaptLocation.cs"); + await Assert.That(lineSpan.StartLinePosition.Line).IsEqualTo(7); + } + [Test] public async Task UnsafeNullConditionalReceiverReportsDiagnosticAndSkipsExpression() { @@ -691,7 +1483,7 @@ public static AddressDto ToDto(this Address source) => public static partial class Mapper { - [Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] + [Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static AddressDto? Map(Address source) => source.GetNested()?.ToDto(); } @@ -739,7 +1531,7 @@ public static AddressDto ToDto(this Address source) => public static partial class Mapper { - [Expressive(NullConditionalRewrite = NullConditionalRewrite.None)] + [Projectable(NullConditionalRewrite = NullConditionalRewrite.None)] public static AddressDto? Map(Address? source) => source?.ToDto(); } diff --git a/tests/Experiments/ExtensionMethodInliningTests.cs b/tests/Experiments/ExtensionMethodInliningTests.cs index 70e9724..c764dc4 100644 --- a/tests/Experiments/ExtensionMethodInliningTests.cs +++ b/tests/Experiments/ExtensionMethodInliningTests.cs @@ -1,4 +1,4 @@ -using AgileObjects.ReadableExpressions; +using AgileObjects.ReadableExpressions; using AlephMapper; namespace Experiments; @@ -42,7 +42,7 @@ public class ExtensionTestPersonDto public static partial class ExtensionTestAddressMapper { - [Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] + [Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] public static ExtensionTestAddressDto ToDto(this ExtensionTestAddress address) => new() { Street = address.Street, @@ -55,22 +55,22 @@ public static partial class ExtensionTestAddressMapper // Main mapper that uses conditional access extension method public static partial class ConditionalExtensionTestPersonMapper { - [Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] + [Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)] [Updatable] public static ExtensionTestPersonDto ToDto(ExtensionTestPerson person) => new() { - Name = person?.Name, - Addresses = person?.Addresses.Select(ExtensionTestAddressMapper.ToDto).ToList(), + Name = person.Name, + Addresses = person?.Addresses?.Select(ExtensionTestAddressMapper.ToDto).ToList(), HomeAddress = person?.HomeAddress?.ToDto(), MyProperty = person?.MyProperty ?? 0 }; - [Expressive(NullConditionalRewrite = NullConditionalRewrite.Ignore)] + [Projectable(NullConditionalRewrite = NullConditionalRewrite.Ignore)] public static ExtensionTestPersonDto ToDto1(ExtensionTestPerson person) => new() { - Name = person?.Name, - Addresses = person?.Addresses?.Select(a => a.ToDto()).ToList(), - HomeAddress = person?.HomeAddress.ToDto(), + Name = person.Name, + Addresses = person.Addresses!.Select(a => a.ToDto()).ToList(), + HomeAddress = person.HomeAddress!.ToDto(), MyProperty = person?.MyProperty ?? 0 }; } @@ -101,4 +101,4 @@ public async Task ConditionalAccessExtensionMethodShouldBeInlined() Console.WriteLine("Generated Expression (Conditional Access):"); Console.WriteLine(readable); } -} \ No newline at end of file +}