commit 79389ab56aa04db0fdb940ed0301a379bf84c1ea Author: Alan Brault Date: Mon Jul 21 13:43:39 2025 -0400 chore: initial commit Signed-off-by: Alan Brault diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..543a92b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,218 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +[*] +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = true + +# Razor files +[*.{cs,razor}] +dotnet_diagnostic.CA2007.severity = none + +# C# files +[*.cs] +#### .NET Coding Conventions #### + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:warning +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = false:warning + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async + +# Code-block preferences +csharp_prefer_braces = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = inside_namespace + +# Constructor Preferences +csharp_style_prefer_primary_constructors = false +dotnet_diagnostic.IDE0290.severity = none +resharper_convert_to_primary_constructor_highlighting = none + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_open_brace = methods, properties, control_blocks, types +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = control_flow_statements, expressions +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Resharper Formatting Rules #### + +resharper_csharp_accessor_declaration_braces = next_line +resharper_csharp_accessor_owner_declaration_braces = next_line +resharper_csharp_align_first_arg_by_paren = true +resharper_csharp_align_linq_query = true +resharper_csharp_align_multiline_argument = true +resharper_csharp_align_multiline_array_and_object_initializer = false +resharper_csharp_align_multiline_binary_expressions_chain = true +resharper_csharp_align_multiline_binary_patterns = true +resharper_csharp_align_multiline_calls_chain = true +resharper_csharp_align_multiline_expression = true +resharper_csharp_align_multiline_extends_list = true +resharper_csharp_align_multiline_parameter = true +resharper_csharp_align_multiline_property_pattern = true +resharper_csharp_align_multiline_statement_conditions = true +resharper_csharp_align_multiline_switch_expression = false +resharper_csharp_align_multiple_declaration = true +resharper_csharp_align_multline_type_parameter_constrains = true +resharper_csharp_align_multline_type_parameter_list = true +resharper_csharp_align_tuple_components = true +resharper_csharp_alignment_tab_fill_style = optimal_fill +resharper_csharp_allow_far_alignment = true +resharper_csharp_brace_style = next_line +resharper_csharp_case_block_braces = next_line +resharper_csharp_continuous_indent_multiplier = 1 +resharper_csharp_indent_anonymous_method_block = true +resharper_csharp_indent_braces_inside_statement_conditions = true +resharper_csharp_indent_inside_namespace = true +resharper_csharp_indent_nested_fixed_stmt = false +resharper_csharp_indent_nested_for_stmt = false +resharper_csharp_indent_nested_foreach_stmt = false +resharper_csharp_indent_nested_lock_stmt = false +resharper_csharp_indent_nested_usings_stmt = false +resharper_csharp_indent_nested_while_stmt = false +resharper_csharp_indent_preprocessor_if = no_indent +resharper_csharp_indent_preprocessor_other = usual_indent +resharper_csharp_indent_preprocessor_region = no_indent +resharper_csharp_indent_type_constraints = true +resharper_csharp_initializer_braces = next_line +resharper_csharp_invocable_declaration_braces = next_line +resharper_csharp_keep_existing_attribute_arrangement = false +resharper_csharp_keep_existing_initializer_arrangement = false +resharper_csharp_max_attribute_length_for_same_line = 120 +resharper_csharp_max_initializer_elements_on_line = 1 +resharper_csharp_naming_rule.enum_member = AaBb +resharper_csharp_other_braces = next_line +resharper_csharp_outdent_binary_ops = true +resharper_csharp_outdent_binary_pattern_ops = true +resharper_csharp_outdent_dots = true +resharper_csharp_place_attribute_on_same_line = false +resharper_csharp_place_record_field_attribute_on_same_line = true +resharper_csharp_place_simple_initializer_on_single_line = true +resharper_csharp_type_declaration_braces = next_line +resharper_csharp_wrap_object_and_collection_initializer_style = chop_always + +dotnet_diagnostic.CA2007.severity = error + +# warning suppressions +dotnet_diagnostic.CA2255.severity = none diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c0288a --- /dev/null +++ b/.gitignore @@ -0,0 +1,406 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +tools/** +!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Rider +.idea/* + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +.DS_Store + +# SonarQube +.sonarqube diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..c7f4bd1 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,49 @@ + + + + Alan Brault + enable + false + 12 + en-US + enable + true + 0.0.1 + + + + true + false + AllEnabledByDefault + + + + embedded + true + true + false + https://git.visus.io/alan.brault/MapperSourceGenSample + true + git + https://git.visus.io/alan.brault/MapperSourceGenSample.git + + + + true + true + + + + $(BeforePack);IncludeAnalyzersInPackage + + + + + <_Parameter1>$(AssemblyName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..b5bc844 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,47 @@ + + + analyzers/dotnet + $(GeneratorProjectBaseTargetPath)/$(AnalyzerLanguage) + $(NoWarn);RS1041 + + + + + $(GeneratorProjectBaseTargetPath) + + + + + + + + + + + <_TargetPathsToSymbols Include="@(_AnalyzerFile)" TargetPath="/%(_AnalyzerFile.PackagePath)" Condition="%(_AnalyzerFile.IsSymbol)" /> + + + + + + + <_AnalyzerPath>analyzers/dotnet + <_AnalyzerPath Condition="'$(AnalyzerRoslynVersion)' != ''">$(_AnalyzerPath)/roslyn$(AnalyzerRoslynVersion) + <_AnalyzerPath Condition="'$(AnalyzerLanguage)' != ''">$(_AnalyzerPath)/$(AnalyzerLanguage) + + + + <_AnalyzerPackFile Include="@(_BuildOutputInPackage->WithMetadataValue('TargetFramework', 'netstandard2.0'))" IsSymbol="false" /> + <_AnalyzerPackFile Include="@(_TargetPathsToSymbols->WithMetadataValue('TargetFramework', 'netstandard2.0'))" IsSymbol="true" /> + <_AnalyzerPackFile PackagePath="$(_AnalyzerPath)/%(TargetPath)" /> + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..f7f5a30 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,19 @@ + + + true + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MapperSourceGenSample.sln b/MapperSourceGenSample.sln new file mode 100644 index 0000000..2cc1e81 --- /dev/null +++ b/MapperSourceGenSample.sln @@ -0,0 +1,57 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36DCA314-4C08-44D2-BE6B-4AD5ACD38A35}" + ProjectSection(SolutionItems) = preProject + Directory.Packages.props = Directory.Packages.props + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01. Foundation", "01. Foundation", "{F327E906-C84C-4C79-9FDC-A8BA45ADCA7D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen.SourceGenerator", "src\MapperSourceGen.SourceGenerator\MapperSourceGen.SourceGenerator.csproj", "{A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02. Sample", "02. Sample", "{62B5A0CF-AB5F-4BF1-8D29-E22AB7DC6348}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03. Unit Tests", "03. Unit Tests", "{9E52F931-B3B3-4F6B-9EDF-89CFE8DC031C}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen", "src\MapperSourceGen\MapperSourceGen.csproj", "{16ADA529-B8A5-43EB-957C-699BF654F894}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen.Sample", "sample\MapperSourceGen.Sample\MapperSourceGen.Sample.csproj", "{9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapperSourceGen.SourceGenerator.Tests", "tests\MapperSourceGen.SourceGenerator.Tests\MapperSourceGen.SourceGenerator.Tests.csproj", "{0B318171-7E7A-4283-9B40-EA97E7ECB728}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394} = {F327E906-C84C-4C79-9FDC-A8BA45ADCA7D} + {16ADA529-B8A5-43EB-957C-699BF654F894} = {F327E906-C84C-4C79-9FDC-A8BA45ADCA7D} + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4} = {62B5A0CF-AB5F-4BF1-8D29-E22AB7DC6348} + {0B318171-7E7A-4283-9B40-EA97E7ECB728} = {9E52F931-B3B3-4F6B-9EDF-89CFE8DC031C} + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2AA5839-C781-4E1A-B0AF-67F5BDD5C394}.Release|Any CPU.Build.0 = Release|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16ADA529-B8A5-43EB-957C-699BF654F894}.Release|Any CPU.Build.0 = Release|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9ECCD86F-9E77-41C3-9494-A8E2C40BF5B4}.Release|Any CPU.Build.0 = Release|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B318171-7E7A-4283-9B40-EA97E7ECB728}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MapperSourceGenSample.sln.DotSettings b/MapperSourceGenSample.sln.DotSettings new file mode 100644 index 0000000..741c5ed --- /dev/null +++ b/MapperSourceGenSample.sln.DotSettings @@ -0,0 +1,316 @@ + + True + Built-in: Full Cleanup + Built-in: Full Cleanup + False + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="Non-reorderable types"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" /> + <HasAttribute Name="JetBrains.Annotations.NoReorder" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" Inherited="True" /> + </And> + </HasMember> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <Or> + <Kind Is="Constructor" /> + <And> + <Kind Is="Method" /> + <ImplementsInterface Name="System.IDisposable" /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Order="Constructor" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" /> + </And> + </TypePattern.Match> + <Entry DisplayName="Setup/Teardown Methods"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureSetUpAttribute" Inherited="True" /> + <HasAttribute Name="NUnit.Framework.FixtureTearDownAttribute" Inherited="True" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="All other members" /> + <Entry DisplayName="Test Methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <HasAttribute Name="NUnit.Framework.TestAttribute" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern" RemoveRegions="All" Priority="150"> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Constant or Field Constant"> + <Entry.Match> + <Or> + <Kind Is="Constant" /> + <And> + <Static /> + <Kind Is="Field" /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Field"> + <Entry.Match> + <Kind Is="Field" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Constructor"> + <Entry.Match> + <Kind Is="Constructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Destructor"> + <Entry.Match> + <Kind Is="Destructor" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Delegate"> + <Entry.Match> + <Kind Is="Delegate" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Event"> + <Entry.Match> + <Kind Is="Event" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Enum"> + <Entry.Match> + <Kind Is="Enum" /> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Interface"> + <Entry.Match> + <Kind Is="Interface" /> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Property"> + <Entry.Match> + <Kind Is="Property" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Indexer"> + <Entry.Match> + <Kind Is="Indexer" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Method"> + <Entry.Match> + <Kind Is="Method" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Struct"> + <Entry.Match> + <Kind Is="Struct" /> + </Entry.Match> + <Entry.SortBy> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Group by Access"> + <Group.GroupBy> + <Access /> + </Group.GroupBy> + <Entry DisplayName="Class"> + <Entry.Match> + <Kind Is="Class" /> + </Entry.Match> + <Entry.SortBy> + <Static /> + <Sealed /> + <Name /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Record"> + <Entry.Match> + <Kind Is="Record" /> + </Entry.Match> + <Entry.SortBy> + <Readonly /> + <Name /> + </Entry.SortBy> + </Entry> + </Group> + </TypePattern> +</Patterns> +True +True +True +True +True +True + True +True +True + True + True + True + True + True + True + True + True + True + True + diff --git a/README.md b/README.md new file mode 100644 index 0000000..823fff6 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# Mapper Source Generator Example + +This project demonstrates how you can use incremental source generators in C# to generate domain transfer objects (DTOs) +and mapping classes from a POCO. + +> [!WARNING] +> This is a sample project and not intended for production use. It is meant to illustrate the concept of using source generators. +> Please look into using either [mapster's](https://github.com/MapsterMapper/Mapster) code generator or [mapperly](https://github.com/riok/mapperly) instead. + +## Requirements +- .NET 8.0 SDK + +## What's Included +- `MapperSourceGen`: The main project that contains the attributes for decorating classes and properties for the source generator. +- `MapperSourceGen.Sample`: A sample class library that demonstrates the use of the source generator. +- `MapperSourceGen.SourceGenerator`: The source generator that generates DTOs and mapping classes. +- `MapperSourceGen.SourceGenerator.Tests`: Unit tests for the source generator to ensure it works as expected (snapshot testing). + +Feel free to inspect the `.csproj` files for the projects to see how they are set up and how the source generator is integrated. + +> [!NOTE] +> The source files emitted by the source generator will be under `Dependencies -> .NET 8.0 -> Source Generators` in the `MapperSourceGen.Sample` project if using Rider. + +## Purpose + +As projects grow, the need for transferring data between layers (like from a database to a UI) becomes essential. +However, with that comes the additional complexity of maintaining DTOs and mapping classes. One of the ways this can be +solved is by using source generators to automate the creation of these classes. + +Source generators provide the ability to generate code at compile time, which can help reduce boilerplate code and improve maintainability +and reduce the reliance on expensive reflection-based mapping. + +## How It Works + +The project **MapperSourceGen.SourceGenerator** contains a source generator that scans for classes decorated with the `GenerateMapperAttribute`. +When it finds such a class, it generates a DTO and a mapping class for it. + +### Example + +Let's say you have a class `Order` that you want to generate a DTO and mapping class for. You would decorate it with the `GenerateMapperAttribute` like this: + +**Order.cs** +```csharp +[Mapper] +public sealed class Order +{ + [MapAlias("EntityId")] // renames Id to EntityId in the DTO + public Guid Id { get; set; } + + public Guid? CustomerId { get; set; } + + [MapAlias("OrderId")] // renames IncrementId to OrderId in the DTO + public int IncrementId { get; set;} + + public DateTimeOffset Created { get; set;} + + public DateTimeOffset? Updated { get; set; } + + [MapIgnore] // this property will not be included in the DTO or mapper + public string TransactionId { get; set; } +} +``` + +The source generator will then generate a DTO class `OrderDto` and a mapping class `OrderMapper` that looks like this: + +**OrderMapper.g.cs** +```csharp +public sealed partial class OrderMapper +{ + public static Order ToModel(OrderDto source) + { + ArgumentNullException.ThrowIfNull(source); + + return new Order() + { + Id = source.EntityId, + CustomerId = source.CustomerId, + IncrementId = source.OrderId + Created = source.Created, + Updated = source.Updated + } + } +} +``` + +**OrderDto.g.cs** +```csharp +public sealed partial class OrderDto +{ + public Guid? CustomerId { get; set; } + + public Guid EntityId { get; set; } + + public int OrderId { get; set; } + + public DateTimeOffset Created { get; set; } + + public DateTimeOffset? Updated { get; set; } +} +``` + + + diff --git a/global.json b/global.json new file mode 100644 index 0000000..23cc917 --- /dev/null +++ b/global.json @@ -0,0 +1,8 @@ +{ + "sdk": { + "version": "8.0.412", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} + diff --git a/sample/MapperSourceGen.Sample/Domain/Customers/Customer.cs b/sample/MapperSourceGen.Sample/Domain/Customers/Customer.cs new file mode 100644 index 0000000..6cebe8a --- /dev/null +++ b/sample/MapperSourceGen.Sample/Domain/Customers/Customer.cs @@ -0,0 +1,67 @@ +namespace MapperSourceGen.Sample.Domain.Customers; + +/// +/// Model representing a customer in the system. +/// +[Mapper] +public sealed class Customer +{ + /// + /// Represents the address lines of the customer. + /// + public IReadOnlyList AddressLines { get; set; } = []; + + /// + /// Represents the country of the customer. + /// + public string Country { get; set; } = string.Empty; + + /// + /// Represents the date and time when the customer was created. + /// + [MapIgnore] + public DateTimeOffset Created { get; set; } + + /// + /// Represents the unique identifier for the customer. + /// + public Guid CustomerId { get; set; } + + /// + /// Represents the email address of the customer. + /// + public string Email { get; set; } = string.Empty; + + /// + /// Represents the first name of the customer. + /// + public string FirstName { get; set; } = string.Empty; + + /// + /// Represents the last name of the customer. + /// + public string LastName { get; set; } = string.Empty; + + /// + /// Represents the phone number of the customer. + /// + public string Phone { get; set; } = string.Empty; + + /// + /// Represents the postal code of the customer. + /// + [MapAlias("ZipCode")] + public string PostalCode { get; set; } = string.Empty; + + /// + /// Represents the state or province of the customer. + /// + [MapAlias("State")] + public string StateOrProvince { get; set; } = string.Empty; + + /// + /// Represents the date and time when the customer was last updated (nullable). + /// + [MapIgnore] + public DateTimeOffset? Updated { get; set; } +} diff --git a/sample/MapperSourceGen.Sample/Domain/Orders/Order.cs b/sample/MapperSourceGen.Sample/Domain/Orders/Order.cs new file mode 100644 index 0000000..e7270e6 --- /dev/null +++ b/sample/MapperSourceGen.Sample/Domain/Orders/Order.cs @@ -0,0 +1,36 @@ +namespace MapperSourceGen.Sample.Domain.Orders; + +/// +/// Model representing an order in the system. +/// +[Mapper] +public sealed class Order +{ + /// + /// Represents the date and time when the order was created. + /// + public DateTimeOffset Created { get; set; } + + /// + /// Represents the unique identifier for the customer associated with the order (nullable). + /// + public Guid? CustomerId { get; set; } + + /// + /// Represents the unique identifier for the order (private). + /// + [MapAlias("EntityId")] + public Guid Id { get; set; } + + /// + /// Represents the unique identifier integer for the order (public). + /// + [MapAlias("OrderId")] + public int IncrementId { get; set; } + + /// + /// Represents the date and time when the order was last updated. + /// + [MapIgnore] + public DateTimeOffset? Updated { get; set; } +} diff --git a/sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj b/sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj new file mode 100644 index 0000000..eec6f12 --- /dev/null +++ b/sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + + + + + + + + + + diff --git a/sample/MapperSourceGen.Sample/packages.lock.json b/sample/MapperSourceGen.Sample/packages.lock.json new file mode 100644 index 0000000..617b656 --- /dev/null +++ b/sample/MapperSourceGen.Sample/packages.lock.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "mappersourcegen": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/MapperSourceGen.SourceGenerator/Extensions/TypeSymbolExtensions.cs b/src/MapperSourceGen.SourceGenerator/Extensions/TypeSymbolExtensions.cs new file mode 100644 index 0000000..d53252a --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Extensions/TypeSymbolExtensions.cs @@ -0,0 +1,24 @@ +namespace MapperSourceGen.SourceGenerator.Extensions; + +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +/// +/// Extension methods for . +/// +internal static class TypeSymbolExtensions +{ + /// + /// Converts an to a . + /// + /// The source instance to convert. + /// An instance of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TypeSyntax ToTypeSyntax(this ITypeSymbol source) + { + string typeName = source.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + return SyntaxFactory.ParseTypeName(typeName); + } +} diff --git a/src/MapperSourceGen.SourceGenerator/MapperGenerator.cs b/src/MapperSourceGen.SourceGenerator/MapperGenerator.cs new file mode 100644 index 0000000..4496617 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/MapperGenerator.cs @@ -0,0 +1,215 @@ +namespace MapperSourceGen.SourceGenerator; + +using System.Collections.Immutable; +using System.Text; +using Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Model; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +/// +/// Incremental source generator that is responsible for generating domain transfer objects (DTOs) and mapper from a +/// class decorated with the MapperAttribute. +/// +[Generator] +public sealed class MapperGenerator : IIncrementalGenerator +{ + private const string MapIgnoreAttributeName = "MapperSourceGen.MapIgnoreAttribute"; + + private const string MapperAttributeName = "MapperSourceGen.MapperAttribute"; + + private const string Source = "source"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // This configures the generator pipeline to look for classes decorated with the MapperAttribute which will then extract + // the hierarchy information and properties to generate DTOs and mapping methods. + IncrementalValuesProvider<(HierarchyInfo Hierarchy, ImmutableArray Properties)> items = + context.SyntaxProvider + .ForAttributeWithMetadataName(MapperAttributeName, + static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + static (ctx, _) => + { + if ( ctx.TargetSymbol is not INamedTypeSymbol classSymbol ) + { + return default; + } + + HierarchyInfo hierarchy = HierarchyInfo.From(classSymbol); + ImmutableArray properties = + [ + ..classSymbol.GetMembers() + .OfType() + .Where(w => !w.IsIndexer + && w is { IsAbstract: false, DeclaredAccessibility: Accessibility.Public } + && !w.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == MapIgnoreAttributeName)) + .Select(s => new PropertyInfo(s)) + ]; + + + return ( hierarchy, properties ); + }); + + // This registers the source output for generating the domain transfer object (DTO). + context.RegisterSourceOutput(items, + static (ctx, item) => + { + ImmutableArray properties = [..GenerateAutoProperties(item.Properties)]; + + CompilationUnitSyntax? compilationUnit = item.Hierarchy.GetCompilationUnit(properties, suffix: "Dto"); + if ( compilationUnit is not null ) + { + ctx.AddSource($"{item.Hierarchy.FileNameHint}Dto.g", compilationUnit.GetText(Encoding.UTF8)); + } + }); + + // This registers the source output for generating the mapper class with mapping methods. + context.RegisterSourceOutput(items, + static (ctx, item) => + { + ImmutableArray declarations = + [ + GenerateMapFromMethod(item.Hierarchy, item.Properties), + GenerateMapToMethod(item.Hierarchy, item.Properties) + ]; + + CompilationUnitSyntax? compilationUnit = item.Hierarchy.GetCompilationUnit(declarations, suffix: "Mapper"); + if ( compilationUnit is not null ) + { + ctx.AddSource($"{item.Hierarchy.FileNameHint}Mapper.g", compilationUnit.GetText(Encoding.UTF8)); + } + }); + } + + /// + /// Generates an expression statement that validates the argument for null. + /// + /// + /// An expression statement that will output ArgumentNullException.ThrowIfNull(nameof(source)); + /// + private static ExpressionStatementSyntax GenerateArgumentValidator() + { + MemberAccessExpressionSyntax member = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("ArgumentNullException"), + IdentifierName("ThrowIfNull")); + + IdentifierNameSyntax nameofIdentifier = IdentifierName(Identifier(TriviaList(), + SyntaxKind.NameOfKeyword, + "nameof", + "nameof", + TriviaList())); + + ArgumentSyntax argument = Argument(InvocationExpression(nameofIdentifier) + .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument(IdentifierName(Source)))))); + + return ExpressionStatement(InvocationExpression(member) + .WithArgumentList(ArgumentList(SingletonSeparatedList(argument)))); + } + + /// + /// Generates auto properties used in the domain transfer object (DTO). + /// + /// An collection of properties to generate auto-properties for. + /// + /// An of items that will output auto-properties: + /// + /// public string Name { get; set; + /// + /// + private static IEnumerable GenerateAutoProperties(IEnumerable properties) + { + return properties.Select(property => PropertyDeclaration(property.PropertyType.ToTypeSyntax(), property.TargetPropertyName) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + )); + } + + /// + /// Generates a method that maps a DTO to a model. + /// + /// An instance that contains information about the model. + /// An of properties to be used in mapping the DTO to the model. + /// + /// An instance of containing the method that returns an instance of the + /// model. + /// + /// Properties that are decorated with the MapIgnoreAttribute will not be included in the mapping. + private static MethodDeclarationSyntax GenerateMapFromMethod(HierarchyInfo hierarchy, ImmutableArray properties) + { + ParameterSyntax parameter = Parameter(Identifier(Source)) + .WithType(ParseTypeName($"{hierarchy.FileNameHint}Dto")); + + ReturnStatementSyntax returnStatement = ReturnStatement(GenerateMappingMethod(hierarchy, properties, isSource: false)); + + return MethodDeclaration(ParseTypeName($"{hierarchy.FileNameHint}"), "ToModel") + .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters(parameter) + .WithBody(Block(GenerateArgumentValidator(), returnStatement)); + } + + private static ObjectCreationExpressionSyntax GenerateMappingMethod(HierarchyInfo hierarchy, + ImmutableArray properties, + string? suffix = null, + bool isSource = true) + { + return ObjectCreationExpression(IdentifierName(ParseTypeName($"{hierarchy.FileNameHint}{suffix}").ToString())) + .WithArgumentList(ArgumentList()) + .WithInitializer(InitializerExpression(SyntaxKind.ObjectInitializerExpression, + SeparatedList(GeneratePropertyAssignments(properties, isSource)))); + } + + /// + /// Generates a method that maps a model to a DTO. + /// + /// An instance that contains information about the model. + /// An of properties to be used in mapping the model to the DTO. + /// + /// An instance of containing the method that returns an instance of the + /// DTO. + /// + /// Properties that are decorated with the MapIgnoreAttribute will not be included in the mapping. + private static MethodDeclarationSyntax GenerateMapToMethod(HierarchyInfo hierarchy, ImmutableArray properties) + { + ParameterSyntax parameter = Parameter(Identifier(Source)) + .WithType(ParseTypeName(hierarchy.FileNameHint)); + + ReturnStatementSyntax returnStatement = ReturnStatement(GenerateMappingMethod(hierarchy, properties, "Dto")); + + return MethodDeclaration(ParseTypeName($"{hierarchy.FileNameHint}Dto"), "ToDto") + .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters(parameter) + .WithBody(Block(GenerateArgumentValidator(), returnStatement)); + } + + /// + /// Generates property assignments for the object initializer in the mapping methods. + /// + /// An of properties that are to be included in the initializer. + /// + /// Determines whether the MapAliasAttribute is to be used on the left or right side of + /// assignment. + /// + /// + /// An containing instances for property + /// assignment. + /// + private static IEnumerable GeneratePropertyAssignments(IEnumerable properties, bool isSource = true) + { + return properties.SelectMany(s => new SyntaxNodeOrToken[] + { + AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, + IdentifierName(!isSource ? s.SourcePropertyName : s.TargetPropertyName), + MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(Source), + IdentifierName(isSource ? s.SourcePropertyName : s.TargetPropertyName))), + Token(SyntaxKind.CommaToken) + }); + } +} diff --git a/src/MapperSourceGen.SourceGenerator/MapperSourceGen.SourceGenerator.csproj b/src/MapperSourceGen.SourceGenerator/MapperSourceGen.SourceGenerator.csproj new file mode 100644 index 0000000..1a4b14c --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/MapperSourceGen.SourceGenerator.csproj @@ -0,0 +1,31 @@ + + + + + netstandard2.0 + + + + + cs + + + true + + + true + + + true + + + + + + + + + diff --git a/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.Syntax.cs b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.Syntax.cs new file mode 100644 index 0000000..6bfc960 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.Syntax.cs @@ -0,0 +1,71 @@ +// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet) + +namespace MapperSourceGen.SourceGenerator.Model; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +internal partial class HierarchyInfo +{ + /// + /// Gets a representing the type hierarchy with the specified member declarations. + /// + /// + /// The instances to include in the + /// . + /// + /// The prefix to prepend to the file and class name. + /// The suffix to append to the file and class name. + /// An instance of used for generating the final output. + public CompilationUnitSyntax GetCompilationUnit(ImmutableArray memberDeclarations, string? prefix = null, string? suffix = null) + { + TypeDeclarationSyntax typeDeclarationSyntax = + Hierarchy[0].GetSyntax(prefix, suffix) + .AddModifiers(Token(TriviaList(Comment("/// ")), + Hierarchy[0].AccessibilityKind, + TriviaList())) + .AddModifiers(GetKeywordModifierTokens(Hierarchy[0])) + .AddMembers([.. memberDeclarations]) + .NormalizeWhitespace(); + + foreach ( TypeInfo parentType in Hierarchy.AsSpan().Slice(1) ) + { + typeDeclarationSyntax = + parentType.GetSyntax(prefix, suffix) + .AddModifiers(Token(TriviaList(Comment("/// ")), + parentType.AccessibilityKind, + TriviaList())) + .AddModifiers(GetKeywordModifierTokens(parentType)) + .AddMembers(typeDeclarationSyntax) + .NormalizeWhitespace(); + } + + SyntaxTriviaList syntaxTriviaList = TriviaList( + Comment("// "), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))); + + return CompilationUnit() + .AddMembers(NamespaceDeclaration(IdentifierName(Namespace)) + .WithLeadingTrivia(syntaxTriviaList) + .AddMembers(typeDeclarationSyntax)) + .NormalizeWhitespace(); + } + + private static SyntaxToken[] GetKeywordModifierTokens(TypeInfo typeInfo) + { + HashSet tokens = []; + + if ( typeInfo.IsSealed ) + { + tokens.Add(Token(SyntaxKind.SealedKeyword)); + } + + tokens.Add(Token(SyntaxKind.PartialKeyword)); + + return [.. tokens]; + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs new file mode 100644 index 0000000..9aecab8 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs @@ -0,0 +1,115 @@ +// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet) + +namespace MapperSourceGen.SourceGenerator.Model; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +/// +/// Represents information about a type hierarchy in a source code file. +/// +internal sealed partial class HierarchyInfo : IEquatable +{ + /// + /// Initializes a new instance of the class. + /// + /// The filename hint for the type (including namespace) without extension. + /// The containing namespace for the type. + /// The current hierarchy for the type. + /// is null or empty. + private HierarchyInfo(string fileNameHint, string @namespace, ImmutableArray hierarchy) + { + if ( string.IsNullOrWhiteSpace(fileNameHint) ) + { + throw new ArgumentException($"'{nameof(fileNameHint)}' cannot be null or empty.", nameof(fileNameHint)); + } + + FileNameHint = fileNameHint; + Hierarchy = hierarchy; + Namespace = @namespace; + } + + /// + /// Gets the file name hint (including full namespace) for the type hierarchy. + /// + public string FileNameHint { get; } + + /// + /// Gets a collection of representing the hierarchy of types. + /// + public ImmutableArray Hierarchy { get; } + + /// + /// Gets the namespace of the type hierarchy. + /// + public string Namespace { get; } + + public static HierarchyInfo From(INamedTypeSymbol typeSymbol) + { + if ( typeSymbol is null ) + { + throw new ArgumentNullException(nameof(typeSymbol)); + } + + LinkedList hierarchy = []; + + for ( INamedTypeSymbol? parent = typeSymbol; + parent is not null; + parent = parent.ContainingType ) + { + hierarchy.AddLast(new TypeInfo(parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind, + parent.DeclaredAccessibility, + parent.IsRecord, + parent.IsSealed)); + } + + return new HierarchyInfo(typeSymbol.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + typeSymbol.ContainingNamespace.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + [..hierarchy]); + } + + public static bool operator ==(HierarchyInfo? left, HierarchyInfo? right) + { + return Equals(left, right); + } + + public static bool operator !=(HierarchyInfo? left, HierarchyInfo? right) + { + return !Equals(left, right); + } + + /// + public bool Equals(HierarchyInfo? other) + { + if ( other is null ) + { + return false; + } + + if ( ReferenceEquals(this, other) ) + { + return true; + } + + return string.Equals(FileNameHint, other.FileNameHint, StringComparison.OrdinalIgnoreCase) + && string.Equals(Namespace, other.Namespace, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || ( obj is HierarchyInfo other && Equals(other) ); + } + + /// + public override int GetHashCode() + { + unchecked + { + return ( StringComparer.OrdinalIgnoreCase.GetHashCode(FileNameHint) * 397 ) + ^ StringComparer.OrdinalIgnoreCase.GetHashCode(Namespace); + } + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs b/src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs new file mode 100644 index 0000000..5bd1b57 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs @@ -0,0 +1,105 @@ +namespace MapperSourceGen.SourceGenerator.Model; + +using Microsoft.CodeAnalysis; + +/// +/// Represents information about a property in a source code file. +/// +internal readonly struct PropertyInfo : IEquatable +{ + private const string MapAliasAttributeName = "MapperSourceGen.MapAliasAttribute"; + + /// + /// Initializes a new instance of the struct. + /// + /// instance of . + /// is null. + public PropertyInfo(IPropertySymbol propertySymbol) + { + if ( propertySymbol is null ) + { + throw new ArgumentNullException(nameof(propertySymbol)); + } + + SourcePropertyName = TargetPropertyName = propertySymbol.Name; + + if ( !propertySymbol.GetAttributes().IsEmpty ) + { + AttributeData? attribute = propertySymbol + .GetAttributes() + .FirstOrDefault(f => f.AttributeClass?.ToDisplayString() == MapAliasAttributeName); + + if ( attribute is not null ) + { + TargetPropertyName = attribute.ConstructorArguments[0].Value!.ToString(); + } + } + + PropertyType = propertySymbol.Type; + } + + /// + /// Gets the type of the property. + /// + public ITypeSymbol PropertyType { get; } + + /// + /// Gets the name of the source property. + /// + public string SourcePropertyName { get; } + + /// + /// Gets the name of the target property (if different from the source). + /// + public string TargetPropertyName { get; } + + /// + /// Indicates whether two instances are equal based on their properties. + /// + /// the first instance to compare + /// the second instance to compare + /// true if both instances are equal, otherwise false. + public static bool operator ==(PropertyInfo left, PropertyInfo right) + { + return left.Equals(right); + } + + /// + /// Indicates whether two instances are not equal based on their properties. + /// + /// the first instance to compare + /// the second instance to compare + /// true if both instances are not equal, otherwise false. + public static bool operator !=(PropertyInfo left, PropertyInfo right) + { + return !left.Equals(right); + } + + /// + public bool Equals(PropertyInfo other) + { + return SymbolEqualityComparer.Default.Equals(PropertyType, other.PropertyType) + && string.Equals(SourcePropertyName, other.SourcePropertyName, StringComparison.OrdinalIgnoreCase) + && string.Equals(TargetPropertyName, other.TargetPropertyName, StringComparison.OrdinalIgnoreCase); + } + + /// + public override bool Equals(object? obj) + { + return obj is PropertyInfo other && Equals(other); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = SymbolEqualityComparer.Default.GetHashCode(PropertyType); + + hashCode = ( hashCode * 397 ) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(SourcePropertyName); + hashCode = ( hashCode * 397 ) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(TargetPropertyName); + + return hashCode; + } + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs b/src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs new file mode 100644 index 0000000..d022306 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs @@ -0,0 +1,48 @@ +// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet) + +namespace MapperSourceGen.SourceGenerator.Model; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +internal sealed record TypeInfo( + string QualifiedName, + TypeKind Kind, + Accessibility DeclaredAccessibility, + bool IsRecord, + bool IsSealed) +{ + public SyntaxKind AccessibilityKind => + DeclaredAccessibility switch + { + Accessibility.Public => SyntaxKind.PublicKeyword, + Accessibility.Internal => SyntaxKind.InternalKeyword, + Accessibility.Private => SyntaxKind.PrivateKeyword, + Accessibility.Protected => SyntaxKind.ProtectedKeyword, + _ => SyntaxKind.None + }; + + public Accessibility DeclaredAccessibility { get; } = DeclaredAccessibility; + + public bool IsRecord { get; } = IsRecord; + + public bool IsSealed { get; } = IsSealed; + + public TypeKind Kind { get; } = Kind; + + public string QualifiedName { get; } = QualifiedName; + + public TypeDeclarationSyntax GetSyntax(string? prefix = null, string? suffix = null) + { + return Kind switch + { + TypeKind.Class when IsRecord => + RecordDeclaration(Token(SyntaxKind.RecordKeyword), $"{prefix}{QualifiedName}{suffix}") + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)), + _ => ClassDeclaration($"{prefix}{QualifiedName}{suffix}") + }; + } +} diff --git a/src/MapperSourceGen.SourceGenerator/Properties/launchSettings.json b/src/MapperSourceGen.SourceGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..78c1508 --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "SourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj" + } + } +} diff --git a/src/MapperSourceGen.SourceGenerator/packages.lock.json b/src/MapperSourceGen.SourceGenerator/packages.lock.json new file mode 100644 index 0000000..2e8244a --- /dev/null +++ b/src/MapperSourceGen.SourceGenerator/packages.lock.json @@ -0,0 +1,121 @@ +{ + "version": 2, + "dependencies": { + ".NETStandard,Version=v2.0": { + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.11.0, 4.11.0]", + "resolved": "4.11.0", + "contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "Microsoft.CodeAnalysis.Common": "[4.11.0]", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "8.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "8.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0", + "System.Memory": "4.5.5" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "Microsoft.CodeAnalysis.Common": { + "type": "CentralTransitive", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "8.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "8.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + } + } + } +} \ No newline at end of file diff --git a/src/MapperSourceGen/MapAliasAttribute.cs b/src/MapperSourceGen/MapAliasAttribute.cs new file mode 100644 index 0000000..e5e102f --- /dev/null +++ b/src/MapperSourceGen/MapAliasAttribute.cs @@ -0,0 +1,27 @@ +namespace MapperSourceGen; + +/// +/// Specifies that the target property should appear in the domain transfer object with a different name. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class MapAliasAttribute : Attribute +{ + private MapAliasAttribute() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name to use instead of the property name. + public MapAliasAttribute(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + Name = name; + } + + /// + /// The name of the property in the domain transfer object. + /// + public string? Name { get; } +} diff --git a/src/MapperSourceGen/MapIgnoreAttribute.cs b/src/MapperSourceGen/MapIgnoreAttribute.cs new file mode 100644 index 0000000..6440568 --- /dev/null +++ b/src/MapperSourceGen/MapIgnoreAttribute.cs @@ -0,0 +1,9 @@ +namespace MapperSourceGen; + +/// +/// Specifies that the target property should not be included in the domain transfer object or mapper. +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class MapIgnoreAttribute : Attribute +{ +} diff --git a/src/MapperSourceGen/MapperAttribute.cs b/src/MapperSourceGen/MapperAttribute.cs new file mode 100644 index 0000000..050f54b --- /dev/null +++ b/src/MapperSourceGen/MapperAttribute.cs @@ -0,0 +1,9 @@ +namespace MapperSourceGen; + +/// +/// Specifies that the target class is a candidate for DTO and mapper generation. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class MapperAttribute : Attribute +{ +} diff --git a/src/MapperSourceGen/MapperSourceGen.csproj b/src/MapperSourceGen/MapperSourceGen.csproj new file mode 100644 index 0000000..ef106ee --- /dev/null +++ b/src/MapperSourceGen/MapperSourceGen.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + + + + + + + + diff --git a/src/MapperSourceGen/packages.lock.json b/src/MapperSourceGen/packages.lock.json new file mode 100644 index 0000000..dd5b66c --- /dev/null +++ b/src/MapperSourceGen/packages.lock.json @@ -0,0 +1,6 @@ +{ + "version": 2, + "dependencies": { + "net8.0": {} + } +} \ No newline at end of file diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..806bea4 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,9 @@ +# C# files +[*.cs] + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..1e2627a --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,16 @@ + + + false + false + enable + true + false + true + + + + true + true + + + diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/Constants.cs b/tests/MapperSourceGen.SourceGenerator.Tests/Constants.cs new file mode 100644 index 0000000..e64902b --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/Constants.cs @@ -0,0 +1,16 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +internal static class Constants +{ + public const string AutoGeneratedStatement = """ + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + """; +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/Fixture.cs b/tests/MapperSourceGen.SourceGenerator.Tests/Fixture.cs new file mode 100644 index 0000000..9f2caea --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/Fixture.cs @@ -0,0 +1,39 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +internal static class Fixture +{ + private static readonly Type[] RequiredAssemblies = + [ + typeof(Binder), + typeof(MapperAttribute) + ]; + + private static IEnumerable AssemblyReferencesForCodeGen => + AppDomain.CurrentDomain + .GetAssemblies() + .Concat(RequiredAssemblies.Select(s => s.Assembly)) + .Distinct() + .Where(w => !w.IsDynamic) + .Select(s => MetadataReference.CreateFromFile(s.Location)); + + public static Task VerifyGenerateSourcesAsync(string source, params IIncrementalGenerator[] generators) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); + + var compilation = CSharpCompilation.Create( + "compilation", + [syntaxTree], + AssemblyReferencesForCodeGen, + new CSharpCompilationOptions(OutputKind.ConsoleApplication)); + + var driver = CSharpGeneratorDriver.Create(generators); + var runner = driver.RunGenerators(compilation); + var verify = Verify(runner); + + return verify; + } +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/MapperGeneratorFacts.cs b/tests/MapperSourceGen.SourceGenerator.Tests/MapperGeneratorFacts.cs new file mode 100644 index 0000000..01b985c --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/MapperGeneratorFacts.cs @@ -0,0 +1,20 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +public sealed class MapperGeneratorFacts +{ + [Fact] + public Task Should_Emit_Dto_And_Mapper() + { + const string source = """ + namespace MapperSourceGen.SourceGenerator.Tests; + + [Mapper] + partial class MyEntity + { + public int Id { get; set; } + } + """; + + return Fixture.VerifyGenerateSourcesAsync(source, new MapperGenerator()); + } +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/MapperSourceGen.SourceGenerator.Tests.csproj b/tests/MapperSourceGen.SourceGenerator.Tests/MapperSourceGen.SourceGenerator.Tests.csproj new file mode 100644 index 0000000..d73b885 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/MapperSourceGen.SourceGenerator.Tests.csproj @@ -0,0 +1,51 @@ + + + + net8.0 + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/ModuleInitializer.cs b/tests/MapperSourceGen.SourceGenerator.Tests/ModuleInitializer.cs new file mode 100644 index 0000000..aff3127 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/ModuleInitializer.cs @@ -0,0 +1,18 @@ +namespace MapperSourceGen.SourceGenerator.Tests; + +using System.Runtime.CompilerServices; +using VerifyTests.DiffPlex; + +public static class ModuleInitializer +{ + #pragma warning disable CA2255 + [ModuleInitializer] + #pragma warning restore CA2255 + public static void Initialize() + { + DerivePathInfo((file, _, type, method) => new PathInfo(Path.Combine(Path.GetDirectoryName(file)!, "ref"), type.Name, method.Name)); + + VerifySourceGenerators.Initialize(); + VerifyDiffPlex.Initialize(OutputType.Compact); + } +} diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/packages.lock.json b/tests/MapperSourceGen.SourceGenerator.Tests/packages.lock.json new file mode 100644 index 0000000..6d16fff --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/packages.lock.json @@ -0,0 +1,254 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "coverlet.collector": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" + }, + "coverlet.msbuild": { + "type": "Direct", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "Qa7Hg+wrOMDKpXVn2dw4Wlun490bIWsFW0fdNJQFJLZnbU27MCP0HJ2mPgS+3EQBQUb0zKlkwiQzP+j38Hc3Iw==" + }, + "JunitXml.TestLogger": { + "type": "Direct", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "a3ciawoHOzqcry7yS5z9DerNyF9QZi6fEZZJPILSy6Noj6+r8Ydma+cENA6wvivXDCblpXxw72wWT9QApNy/0w==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Direct", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "8.0.0", + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Direct", + "requested": "[4.11.0, 4.11.0]", + "resolved": "4.11.0", + "contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "Microsoft.CodeAnalysis.Common": "[4.11.0]", + "System.Collections.Immutable": "8.0.0", + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.14.1, )", + "resolved": "17.14.1", + "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", + "dependencies": { + "Microsoft.CodeCoverage": "17.14.1", + "Microsoft.TestPlatform.TestHost": "17.14.1" + } + }, + "Verify.DiffPlex": { + "type": "Direct", + "requested": "[3.1.2, )", + "resolved": "3.1.2", + "contentHash": "ySaQ+MffcDfGWzBXB9UHppEGBqzl0L+2CxZcT04xQ3gugsN5AAjBPHkt75Ca61PlAeZCyty/p/Q9ZwaQjNOoTg==", + "dependencies": { + "DiffPlex": "1.7.2", + "Verify": "27.0.0" + } + }, + "Verify.SourceGenerators": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "XhAg+fJDPXDH7Ajv/J4Hv8ls0zoeK0LqjZIiOT+quwxOqdplcTuqdPx1+4p1qvYzpTdwkLxyGiIA76MzCljyAQ==", + "dependencies": { + "Verify": "26.5.0" + } + }, + "Verify.Xunit": { + "type": "Direct", + "requested": "[30.5.0, )", + "resolved": "30.5.0", + "contentHash": "S+vPvbWgcZSR/eF5O8OAlz8uXhB2Dr2uKjUKbPLa9FtC5ay0hueYGzKshHmRZJ1jay88VE1Kd2ECPNhb8p3Uyg==", + "dependencies": { + "Argon": "0.30.1", + "DiffEngine": "16.2.3", + "SimpleInfoName": "3.1.2", + "Verify": "30.5.0", + "xunit.abstractions": "2.0.3", + "xunit.extensibility.execution": "2.9.3" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.9.3, )", + "resolved": "2.9.3", + "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", + "dependencies": { + "xunit.analyzers": "1.18.0", + "xunit.assert": "2.9.3", + "xunit.core": "[2.9.3]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.3, )", + "resolved": "3.1.3", + "contentHash": "go7e81n/UI3LeNqoJIJ3thkS4JfJtiQnDbAxLh09JkJqoHthnfbLS5p68s4/Bm12B9umkoYSB5SaDr68hZNleg==" + }, + "Argon": { + "type": "Transitive", + "resolved": "0.30.1", + "contentHash": "kjKnBzxJ1Xp4Sh9B7inrP1YjefXH4X8hV4/J5EoDKloog09Kp4KUVoJS8xxYfUbUzJ+Xe5PKZm3hj5pi4ZuCZw==" + }, + "DiffEngine": { + "type": "Transitive", + "resolved": "16.2.3", + "contentHash": "QWnG0MR3//Ss0G0N9mIfe1HLOrOIRZqau0AOiLt9Gm53ZQf/TLvzoccTkczEW5ACkbhRY5m+p+W7bzFVln2GDw==", + "dependencies": { + "EmptyFiles": "8.10.1", + "System.Management": "8.0.0" + } + }, + "DiffPlex": { + "type": "Transitive", + "resolved": "1.7.2", + "contentHash": "qJEjdxEDBWSFZGB8paBB9HDeJXHGlHlOXeGX3kbTuXWuOsgv2iSAEOOzo5V1/B39Vcxr9IVVrNKewRcX+rsn4g==" + }, + "EmptyFiles": { + "type": "Transitive", + "resolved": "8.10.1", + "contentHash": "vhLPAqdKuo2qjVkrJbCyacGXO9XTha7G1R5amw44m877FDR/gqFjCfdncj8VyHAC6eNqrCXgYTbHJGO5+l3TJg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==", + "dependencies": { + "System.Reflection.Metadata": "8.0.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.14.1", + "contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.14.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "SimpleInfoName": { + "type": "Transitive", + "resolved": "3.1.2", + "contentHash": "/OoEZQxSW6DeTJ9nfrg8BLCOCWpxBiWHV4NkG3t+Xpe8tvzm7yCwKwxkhpauMl3fg9OjlIjJMKX61H6VavLkrw==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "WTlRjL6KWIMr/pAaq3rYqh0TJlzpouaQ/W1eelssHgtlwHAH25jXTkUphTYx9HaIIf7XA6qs/0+YhtLEQRkJ+Q==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "jrK22i5LRzxZCfGb+tGmke2VH7oE0DvcDlJ1HAKYU8cPmD8XnpUT0bYn2Gy98GEhGjtfbR/sxKTVb+dE770pfA==", + "dependencies": { + "System.CodeDom": "8.0.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0" + } + }, + "Verify": { + "type": "Transitive", + "resolved": "30.5.0", + "contentHash": "AO3l7Rmw8Ry8rpVMQ98Q3MVO2G0KGiXlxyOPAYuE7NeN33JzNLJSjri/fMQDo3sm4aIwBjWbomssy2EbyJBdrg==", + "dependencies": { + "Argon": "0.30.1", + "DiffEngine": "16.2.3", + "SimpleInfoName": "3.1.2" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.18.0", + "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]", + "xunit.extensibility.execution": "[2.9.3]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", + "dependencies": { + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.9.3", + "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", + "dependencies": { + "xunit.extensibility.core": "[2.9.3]" + } + }, + "mappersourcegen": { + "type": "Project" + }, + "mappersourcegen.sourcegenerator": { + "type": "Project", + "dependencies": { + "Microsoft.CodeAnalysis.CSharp": "[4.11.0, 4.11.0]" + } + } + } + } +} \ No newline at end of file diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.verified.cs b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.verified.cs new file mode 100644 index 0000000..11dd2a9 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: MapperSourceGen.SourceGenerator.Tests.MyEntityDto.g.cs +// +#pragma warning disable +#nullable enable +namespace MapperSourceGen.SourceGenerator.Tests +{ + /// + internal partial class MyEntityDto + { + public int Id { get; set; } + } +} \ No newline at end of file diff --git a/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.verified.cs b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.verified.cs new file mode 100644 index 0000000..09eb290 --- /dev/null +++ b/tests/MapperSourceGen.SourceGenerator.Tests/ref/MapperGeneratorFacts.Should_Emit_Dto_And_Mapper#MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.verified.cs @@ -0,0 +1,28 @@ +//HintName: MapperSourceGen.SourceGenerator.Tests.MyEntityMapper.g.cs +// +#pragma warning disable +#nullable enable +namespace MapperSourceGen.SourceGenerator.Tests +{ + /// + internal partial class MyEntityMapper + { + public static MapperSourceGen.SourceGenerator.Tests.MyEntity ToModel(MapperSourceGen.SourceGenerator.Tests.MyEntityDto source) + { + ArgumentNullException.ThrowIfNull(nameof(source)); + return new MapperSourceGen.SourceGenerator.Tests.MyEntity() + { + Id = source.Id, + }; + } + + public static MapperSourceGen.SourceGenerator.Tests.MyEntityDto ToDto(MapperSourceGen.SourceGenerator.Tests.MyEntity source) + { + ArgumentNullException.ThrowIfNull(nameof(source)); + return new MapperSourceGen.SourceGenerator.Tests.MyEntityDto() + { + Id = source.Id, + }; + } + } +} \ No newline at end of file