diff --git a/.editorconfig b/.editorconfig index 273fd18..aa6b134 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,72 +1,72 @@ - -[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] -indent_style = space -indent_size = 4 -tab_width = 4 - -[*] - -# Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers = false -csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion -csharp_space_after_cast = false -csharp_style_var_elsewhere = true:suggestion -csharp_style_var_for_built_in_types = true:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none -dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none -dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion -dotnet_style_qualification_for_event = false:suggestion -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion - -# ReSharper properties -resharper_autodetect_indent_settings = true -resharper_blank_lines_after_control_transfer_statements = 1 -resharper_blank_lines_after_multiline_statements = 1 -resharper_blank_lines_around_block_case_section = 1 -resharper_blank_lines_around_multiline_case_section = 1 -resharper_blank_lines_around_single_line_auto_property = 1 -resharper_blank_lines_around_single_line_local_method = 1 -resharper_blank_lines_around_single_line_property = 1 -resharper_braces_for_for = required -resharper_braces_for_foreach = required -resharper_braces_for_ifelse = required -resharper_braces_for_while = required -resharper_csharp_blank_lines_around_single_line_invocable = 1 -resharper_csharp_empty_block_style = together_same_line -resharper_csharp_keep_blank_lines_in_code = 1 -resharper_csharp_keep_blank_lines_in_declarations = 1 -resharper_csharp_max_line_length = 180 -resharper_csharp_wrap_lines = false -resharper_local_function_body = expression_body -resharper_method_or_operator_body = expression_body -resharper_place_accessorholder_attribute_on_same_line = false -resharper_place_field_attribute_on_same_line = false -resharper_space_after_cast = false -resharper_space_within_single_line_array_initializer_braces = true -resharper_use_indent_from_vs = false -resharper_xmldoc_indent_text = ZeroIndent - -# ReSharper inspection severities -resharper_arguments_style_literal_highlighting = none -resharper_arguments_style_named_expression_highlighting = none -resharper_arguments_style_other_highlighting = none -resharper_arrange_redundant_parentheses_highlighting = hint -resharper_arrange_this_qualifier_highlighting = hint -resharper_arrange_type_member_modifiers_highlighting = hint -resharper_arrange_type_modifiers_highlighting = hint -resharper_built_in_type_reference_style_for_member_access_highlighting = hint -resharper_built_in_type_reference_style_highlighting = hint -resharper_class_never_instantiated_global_highlighting = none -resharper_redundant_base_qualifier_highlighting = warning -resharper_suggest_var_or_type_built_in_types_highlighting = hint -resharper_suggest_var_or_type_elsewhere_highlighting = hint -resharper_suggest_var_or_type_simple_types_highlighting = hint -resharper_web_config_module_not_resolved_highlighting = warning -resharper_web_config_type_not_resolved_highlighting = warning -resharper_web_config_wrong_module_highlighting = warning + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 4 +tab_width = 4 + +[*] + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers = false +csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_space_after_cast = false +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_autodetect_indent_settings = true +resharper_blank_lines_after_control_transfer_statements = 1 +resharper_blank_lines_after_multiline_statements = 1 +resharper_blank_lines_around_block_case_section = 1 +resharper_blank_lines_around_multiline_case_section = 1 +resharper_blank_lines_around_single_line_auto_property = 1 +resharper_blank_lines_around_single_line_local_method = 1 +resharper_blank_lines_around_single_line_property = 1 +resharper_braces_for_for = required +resharper_braces_for_foreach = required +resharper_braces_for_ifelse = required +resharper_braces_for_while = required +resharper_csharp_blank_lines_around_single_line_invocable = 1 +resharper_csharp_empty_block_style = together_same_line +resharper_csharp_keep_blank_lines_in_code = 1 +resharper_csharp_keep_blank_lines_in_declarations = 1 +resharper_csharp_max_line_length = 180 +resharper_csharp_wrap_lines = false +resharper_local_function_body = expression_body +resharper_method_or_operator_body = expression_body +resharper_place_accessorholder_attribute_on_same_line = false +resharper_place_field_attribute_on_same_line = false +resharper_space_after_cast = false +resharper_space_within_single_line_array_initializer_braces = true +resharper_use_indent_from_vs = false +resharper_xmldoc_indent_text = ZeroIndent + +# ReSharper inspection severities +resharper_arguments_style_literal_highlighting = none +resharper_arguments_style_named_expression_highlighting = none +resharper_arguments_style_other_highlighting = none +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_class_never_instantiated_global_highlighting = none +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 64b4e64..8066ca6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,28 +1,28 @@ -name: Build and Test - -on: - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - uses: dotnet/nbgv@v0.4.0 - with: - setAllVars: true - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.101 - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal +name: Build and Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - uses: dotnet/nbgv@v0.4.0 + with: + setAllVars: true + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.101 + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 20eb58c..9ada7af 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -1,37 +1,37 @@ -name: Publish Packages - -on: - push: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - uses: dotnet/nbgv@v0.4.0 - with: - setAllVars: true - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.101 - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --configuration Release --no-restore /p:PublicRelease=true - - name: Test - run: dotnet test --configuration Release --no-build --verbosity normal - - name: Publish MapTo - uses: brandedoutcast/publish-nuget@v2.5.5 - with: - PROJECT_FILE_PATH: src/MapTo/MapTo.csproj - NUGET_KEY: ${{secrets.NUGET_API_KEY}} - NUGET_SOURCE: https://api.nuget.org - TAG_COMMIT: false - INCLUDE_SYMBOLS: true - VERSION_STATIC: ${{env.NBGV_SemVer1}} +name: Publish Packages + +on: + push: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + - uses: dotnet/nbgv@v0.4.0 + with: + setAllVars: true + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.101 + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore /p:PublicRelease=true + - name: Test + run: dotnet test --configuration Release --no-build --verbosity normal + - name: Publish MapTo + uses: brandedoutcast/publish-nuget@v2.5.5 + with: + PROJECT_FILE_PATH: src/MapTo/MapTo.csproj + NUGET_KEY: ${{secrets.NUGET_API_KEY}} + NUGET_SOURCE: https://api.nuget.org + TAG_COMMIT: false + INCLUDE_SYMBOLS: true + VERSION_STATIC: ${{env.NBGV_SemVer1}} diff --git a/.gitignore b/.gitignore index 920d1cd..290f4b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,442 +1,442 @@ -## 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/master/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/ -[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/ - -# 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 -*.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, .xml, .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 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/ - -# 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/ - -## -## Visual studio for Mac -## - - -# globs -Makefile.in -*.userprefs -*.usertasks -config.make -config.status -aclocal.m4 -install-sh -autom4te.cache/ -*.tar.gz -tarballs/ -test-results/ - -# Mac bundle stuff -*.dmg -*.app - -# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# JetBrains Rider -.idea/ -*.sln.iml - -## -## Visual Studio Code -## -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json +## 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/master/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/ +[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/ + +# 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 +*.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, .xml, .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 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/ + +# 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/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/LICENSE b/LICENSE index 97d5740..841c74d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2020 Mohammadreza Taikandi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +MIT License + +Copyright (c) 2020 Mohammadreza Taikandi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/MapTo.sln b/MapTo.sln index 0c203d5..881aa75 100644 --- a/MapTo.sln +++ b/MapTo.sln @@ -1,34 +1,34 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\MapTo\MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Tests\MapTo.Tests.csproj", "{797DA57B-AC7E-468B-8799-44C5A574C0E3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.Build.0 = Release|Any CPU - {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.Build.0 = Release|Any CPU - {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.Build.0 = Release|Any CPU - {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\MapTo\MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Tests\MapTo.Tests.csproj", "{797DA57B-AC7E-468B-8799-44C5A574C0E3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.Build.0 = Release|Any CPU + {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.Build.0 = Release|Any CPU + {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.Build.0 = Release|Any CPU + {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/MapTo.sln.DotSettings b/MapTo.sln.DotSettings index 2397ecf..83b264c 100644 --- a/MapTo.sln.DotSettings +++ b/MapTo.sln.DotSettings @@ -1,6 +1,6 @@ - - True - True - True - True + + True + True + True + True True \ No newline at end of file diff --git a/README.md b/README.md index 8db4591..0fe5fee 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,91 @@ -# MapTo -[![Nuget](https://img.shields.io/nuget/v/mapto?logo=nuget)](https://www.nuget.org/packages/MapTo/) -![Publish Packages](https://github.com/mrtaikandi/MapTo/workflows/Publish%20Packages/badge.svg) - -A convention based object to object mapper using [Roslyn source generator](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md). - -MapTo is a library to programmatically generate the necessary code to map one object to another during compile-time, eliminating the need to use reflection to map objects and make it much faster in runtime. It provides compile-time safety checks and ease of use by leveraging extension methods. - - -## Installation -``` -dotnet add package MapTo --prerelease -``` - -## Usage -MapTo relies on a set of attributes to instruct it on how to generate the mappings. To start, declare the destination class as `partial` and annotate it with `MapFrom` attribute. As its name implies, `MapFrom` attribute tells the library what the source class you want to map from is. - -```c# -using MapTo; - -namespace App.ViewModels -{ - [MapFrom(typeof(App.Data.Models.User))] - public partial class UserViewModel - { - public string FirstName { get; } - - public string LastName { get; } - - [IgnoreProperty] - public string FullName { get; set; } - } -} -``` - -To get an instance of `UserViewModel` from the `User` class, you can use any of the following methods: - -```c# -var user = new User(id: 10) { FirstName = "John", LastName = "Doe" }; - -var vm = user.ToUserViewModel(); // A generated extension method for User class. - -// OR -vm = new UserViewModel(user); // A generated contructor. - -// OR -vm = UserViewModel.From(user); // A generated factory method. -``` - -> Please refer to [sample console app](https://github.com/mrtaikandi/MapTo/tree/master/test/TestConsoleApp) for a more complete example. - -## Available Attributes -### IgnoreProperty -By default, MapTo will include all properties with the same name (case-sensitive), whether read-only or not, in the mapping unless annotating them with the `IgnoreProperty` attribute. -```c# -[IgnoreProperty] -public string FullName { get; set; } -``` - -### MapProperty -This attribute gives you more control over the way the annotated property should get mapped. For instance, if the annotated property should use a property in the source class with a different name. - -```c# -[MapProperty(SourcePropertyName = "Id")] -public int Key { get; set; } -``` - -### MapTypeConverter -A compilation error gets raised by default if the source and destination properties types are not implicitly convertible, but to convert the incompatible source type to the desired destination type, `MapTypeConverter` can be used. - -This attribute will accept a type that implements `ITypeConverter` interface. - -```c# -[MapFrom(typeof(User))] -public partial class UserViewModel -{ - public DateTimeOffset RegisteredAt { get; set; } - - [IgnoreProperty] - public ProfileViewModel Profile { get; set; } - - [MapTypeConverter(typeof(IdConverter))] - [MapProperty(SourcePropertyName = nameof(User.Id))] - public string Key { get; } - - private class IdConverter : ITypeConverter - { - public string Convert(int source, object[] converterParameters) => $"{source:X}"; - } -} +# MapTo +[![Nuget](https://img.shields.io/nuget/v/mapto?logo=nuget)](https://www.nuget.org/packages/MapTo/) +![Publish Packages](https://github.com/mrtaikandi/MapTo/workflows/Publish%20Packages/badge.svg) + +A convention based object to object mapper using [Roslyn source generator](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md). + +MapTo is a library to programmatically generate the necessary code to map one object to another during compile-time, eliminating the need to use reflection to map objects and make it much faster in runtime. It provides compile-time safety checks and ease of use by leveraging extension methods. + + +## Installation +``` +dotnet add package MapTo --prerelease +``` + +## Usage +MapTo relies on a set of attributes to instruct it on how to generate the mappings. To start, declare the destination class as `partial` and annotate it with `MapFrom` attribute. As its name implies, `MapFrom` attribute tells the library what the source class you want to map from is. + +```c# +using MapTo; + +namespace App.ViewModels +{ + [MapFrom(typeof(App.Data.Models.User))] + public partial class UserViewModel + { + public string FirstName { get; } + + public string LastName { get; } + + [IgnoreProperty] + public string FullName { get; set; } + } +} +``` + +To get an instance of `UserViewModel` from the `User` class, you can use any of the following methods: + +```c# +var user = new User(id: 10) { FirstName = "John", LastName = "Doe" }; + +var vm = user.ToUserViewModel(); // A generated extension method for User class. + +// OR +vm = new UserViewModel(user); // A generated contructor. + +// OR +vm = UserViewModel.From(user); // A generated factory method. +``` + +> Please refer to [sample console app](https://github.com/mrtaikandi/MapTo/tree/master/test/TestConsoleApp) for a more complete example. + +## Available Attributes +### IgnoreProperty +By default, MapTo will include all properties with the same name (case-sensitive), whether read-only or not, in the mapping unless annotating them with the `IgnoreProperty` attribute. +```c# +[IgnoreProperty] +public string FullName { get; set; } +``` + +### MapProperty +This attribute gives you more control over the way the annotated property should get mapped. For instance, if the annotated property should use a property in the source class with a different name. + +```c# +[MapProperty(SourcePropertyName = "Id")] +public int Key { get; set; } +``` + +### MapTypeConverter +A compilation error gets raised by default if the source and destination properties types are not implicitly convertible, but to convert the incompatible source type to the desired destination type, `MapTypeConverter` can be used. + +This attribute will accept a type that implements `ITypeConverter` interface. + +```c# +[MapFrom(typeof(User))] +public partial class UserViewModel +{ + public DateTimeOffset RegisteredAt { get; set; } + + [IgnoreProperty] + public ProfileViewModel Profile { get; set; } + + [MapTypeConverter(typeof(IdConverter))] + [MapProperty(SourcePropertyName = nameof(User.Id))] + public string Key { get; } + + private class IdConverter : ITypeConverter + { + public string Convert(int source, object[] converterParameters) => $"{source:X}"; + } +} ``` \ No newline at end of file diff --git a/src/MapTo/ClassMappingContext.cs b/src/MapTo/ClassMappingContext.cs index d7edeb0..c8d427d 100644 --- a/src/MapTo/ClassMappingContext.cs +++ b/src/MapTo/ClassMappingContext.cs @@ -1,67 +1,67 @@ -using System.Collections.Immutable; -using System.Linq; -using MapTo.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo -{ - internal class ClassMappingContext : MappingContext - { - internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) - : base(compilation, sourceGenerationOptions, typeSyntax) { } - - protected override ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return typeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapField(sourceTypeSymbol, sourceProperties, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return typeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return sourceTypeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapFieldSimple(typeSymbol, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return sourceTypeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapPropertySimple(typeSymbol, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - } +using System.Collections.Immutable; +using System.Linq; +using MapTo.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo +{ + internal class ClassMappingContext : MappingContext + { + internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) + : base(compilation, sourceGenerationOptions, typeSyntax) { } + + protected override ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return typeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapField(sourceTypeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return typeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return sourceTypeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapFieldSimple(typeSymbol, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return sourceTypeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapPropertySimple(typeSymbol, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + } } \ No newline at end of file diff --git a/src/MapTo/CompilerServices/IsExternalInit.cs b/src/MapTo/CompilerServices/IsExternalInit.cs index e750e2f..72b53ee 100644 --- a/src/MapTo/CompilerServices/IsExternalInit.cs +++ b/src/MapTo/CompilerServices/IsExternalInit.cs @@ -1,16 +1,16 @@ -// ReSharper disable UnusedType.Global -// ReSharper disable CheckNamespace -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; - -namespace System.Runtime.CompilerServices -{ - /// - /// Reserved to be used by the compiler for tracking metadata. - /// This class should not be used by developers in source code. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit { } +// ReSharper disable UnusedType.Global +// ReSharper disable CheckNamespace +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { } } \ No newline at end of file diff --git a/src/MapTo/CompilerServices/NullableAttributes.cs b/src/MapTo/CompilerServices/NullableAttributes.cs index 6886ade..3a00084 100644 --- a/src/MapTo/CompilerServices/NullableAttributes.cs +++ b/src/MapTo/CompilerServices/NullableAttributes.cs @@ -1,178 +1,178 @@ -// ReSharper disable CheckNamespace -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#if NETSTANDARD2_0 -namespace System.Diagnostics.CodeAnalysis -{ -// These attributes already shipped with .NET Core 3.1 in System.Runtime -#if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1 - /// Specifies that null is allowed as an input even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] - internal sealed class AllowNullAttribute : Attribute { } - - /// Specifies that null is disallowed as an input even if the corresponding type allows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] - internal sealed class DisallowNullAttribute : Attribute { } - - /// Specifies that an output may be null even if the corresponding type disallows it. - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] - internal sealed class MaybeNullAttribute : Attribute { } - - /// - /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input - /// argument was not null when the call returns. - /// - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] - internal sealed class NotNullAttribute : Attribute { } - - /// - /// Specifies that when a method returns , the parameter may be null even if the - /// corresponding type disallows it. - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class MaybeNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter may be null. - /// - public MaybeNullWhenAttribute(bool returnValue) - { - ReturnValue = returnValue; - } - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// - /// Specifies that when a method returns , the parameter will not be null even if the - /// corresponding type allows it. - /// - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class NotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - public NotNullWhenAttribute(bool returnValue) - { - ReturnValue = returnValue; - } - - /// Gets the return value condition. - public bool ReturnValue { get; } - } - - /// Specifies that the output will be non-null if the named parameter is non-null. - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)] - internal sealed class NotNullIfNotNullAttribute : Attribute - { - /// Initializes the attribute with the associated parameter name. - /// - /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. - /// - public NotNullIfNotNullAttribute(string parameterName) - { - ParameterName = parameterName; - } - - /// Gets the associated parameter name. - public string ParameterName { get; } - } - - /// Applied to a method that will never return under any circumstance. - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - internal sealed class DoesNotReturnAttribute : Attribute { } - - /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. - [AttributeUsage(AttributeTargets.Parameter)] - internal sealed class DoesNotReturnIfAttribute : Attribute - { - /// Initializes the attribute with the specified parameter value. - /// - /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to - /// the associated parameter matches this value. - /// - public DoesNotReturnIfAttribute(bool parameterValue) - { - ParameterValue = parameterValue; - } - - /// Gets the condition parameter value. - public bool ParameterValue { get; } - } -#endif - - /// - /// Specifies that the method or property will ensure that the listed field and property members have not-null - /// values. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] - internal sealed class MemberNotNullAttribute : Attribute - { - /// Initializes the attribute with a field or property member. - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullAttribute(string member) - { - Members = new[] { member }; - } - - /// Initializes the attribute with the list of field and property members. - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullAttribute(params string[] members) - { - Members = members; - } - - /// Gets field or property member names. - public string[] Members { get; } - } - - /// - /// Specifies that the method or property will ensure that the listed field and property members have not-null - /// values when returning with the specified return value condition. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] - internal sealed class MemberNotNullWhenAttribute : Attribute - { - /// Initializes the attribute with the specified return value condition and a field or property member. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - /// - /// The field or property member that is promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, string member) - { - ReturnValue = returnValue; - Members = new[] { member }; - } - - /// Initializes the attribute with the specified return value condition and list of field and property members. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// - /// - /// The list of field and property members that are promised to be not-null. - /// - public MemberNotNullWhenAttribute(bool returnValue, params string[] members) - { - ReturnValue = returnValue; - Members = members; - } - - /// Gets field or property member names. - public string[] Members { get; } - - /// Gets the return value condition. - public bool ReturnValue { get; } - } -} +// ReSharper disable CheckNamespace +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETSTANDARD2_0 +namespace System.Diagnostics.CodeAnalysis +{ +// These attributes already shipped with .NET Core 3.1 in System.Runtime +#if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] + internal sealed class AllowNullAttribute : Attribute { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] + internal sealed class DisallowNullAttribute : Attribute { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] + internal sealed class MaybeNullAttribute : Attribute { } + + /// + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input + /// argument was not null when the call returns. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] + internal sealed class NotNullAttribute : Attribute { } + + /// + /// Specifies that when a method returns , the parameter may be null even if the + /// corresponding type disallows it. + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// + /// Specifies that when a method returns , the parameter will not be null even if the + /// corresponding type allows it. + /// + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) + { + ParameterName = parameterName; + } + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) + { + ParameterValue = parameterValue; + } + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +#endif + + /// + /// Specifies that the method or property will ensure that the listed field and property members have not-null + /// values. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) + { + Members = new[] { member }; + } + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) + { + Members = members; + } + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// + /// Specifies that the method or property will ensure that the listed field and property members have not-null + /// values when returning with the specified return value condition. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets field or property member names. + public string[] Members { get; } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} #endif \ No newline at end of file diff --git a/src/MapTo/DiagnosticsFactory.cs b/src/MapTo/DiagnosticsFactory.cs index 3b29a8a..bb4fbeb 100644 --- a/src/MapTo/DiagnosticsFactory.cs +++ b/src/MapTo/DiagnosticsFactory.cs @@ -1,42 +1,42 @@ -using System.Linq; -using MapTo.Extensions; -using MapTo.Sources; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using static MapTo.Sources.Constants; - -namespace MapTo -{ - internal static class DiagnosticsFactory - { - private const string UsageCategory = "Usage"; - private const string CodePrefix = "MT"; - private const string ErrorId = CodePrefix + "0"; - private const string InfoId = CodePrefix + "1"; - private const string WarningId = CodePrefix + "2"; - - internal static Diagnostic TypeNotFoundError(Location location, string syntaxName) => - Create($"{ErrorId}010", location, $"Unable to find '{syntaxName}' type."); - - internal static Diagnostic MapFromAttributeNotFoundError(Location location) => - Create($"{ErrorId}020", location, $"Unable to find {MapFromAttributeSource.AttributeName} type."); - - internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) => - Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types."); - - internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) => - Create($"{ErrorId}031", property.Locations.FirstOrDefault(), $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnorePropertyAttributeSource.FullyQualifiedName}'."); - - internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, IPropertySymbol sourceProperty) => - Create($"{ErrorId}032", property.Locations.FirstOrDefault(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.GetTypeSymbol()?.ToDisplayString()}>'."); - - internal static Diagnostic ConfigurationParseError(string error) => - Create($"{ErrorId}040", Location.None, error); - - internal static Diagnostic MissingConstructorArgument(ConstructorDeclarationSyntax constructorSyntax) => - Create($"{ErrorId}050", constructorSyntax.GetLocation(), "There are no argument given that corresponds to the required formal parameter."); - - private static Diagnostic Create(string id, Location? location, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) => - Diagnostic.Create(new DiagnosticDescriptor(id, string.Empty, message, UsageCategory, severity, true), location ?? Location.None); - } +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static MapTo.Sources.Constants; + +namespace MapTo +{ + internal static class DiagnosticsFactory + { + private const string UsageCategory = "Usage"; + private const string CodePrefix = "MT"; + private const string ErrorId = CodePrefix + "0"; + private const string InfoId = CodePrefix + "1"; + private const string WarningId = CodePrefix + "2"; + + internal static Diagnostic TypeNotFoundError(Location location, string syntaxName) => + Create($"{ErrorId}010", location, $"Unable to find '{syntaxName}' type."); + + internal static Diagnostic MapFromAttributeNotFoundError(Location location) => + Create($"{ErrorId}020", location, $"Unable to find {MapFromAttributeSource.AttributeName} type."); + + internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) => + Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types."); + + internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) => + Create($"{ErrorId}031", property.Locations.FirstOrDefault(), $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnoreMemberAttributeSource.FullyQualifiedName}'."); + + internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, IPropertySymbol sourceProperty) => + Create($"{ErrorId}032", property.Locations.FirstOrDefault(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.GetTypeSymbol()?.ToDisplayString()}>'."); + + internal static Diagnostic ConfigurationParseError(string error) => + Create($"{ErrorId}040", Location.None, error); + + internal static Diagnostic MissingConstructorArgument(ConstructorDeclarationSyntax constructorSyntax) => + Create($"{ErrorId}050", constructorSyntax.GetLocation(), "There are no argument given that corresponds to the required formal parameter."); + + private static Diagnostic Create(string id, Location? location, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) => + Diagnostic.Create(new DiagnosticDescriptor(id, string.Empty, message, UsageCategory, severity, true), location ?? Location.None); + } } \ No newline at end of file diff --git a/src/MapTo/Extensions/CommonExtensions.cs b/src/MapTo/Extensions/CommonExtensions.cs index bf43e97..edc2e23 100644 --- a/src/MapTo/Extensions/CommonExtensions.cs +++ b/src/MapTo/Extensions/CommonExtensions.cs @@ -1,51 +1,81 @@ -using MapTo.Sources; -using System; -using System.Collections.Generic; -using System.Text; - -namespace MapTo.Extensions -{ - internal static class CommonExtensions - { - internal static SourceBuilder WriteComment(this SourceBuilder builder, string comment = "") - { - return builder.WriteLine($"// {comment}"); - } - - internal static SourceBuilder WriteModelInfo(this SourceBuilder builder, MappingModel model) - { - return builder - .WriteLine() - .WriteComment($" IsTypeUpdatable {model.IsTypeUpdatable}") - .WriteComment($" HasMappedBaseClass {model.HasMappedBaseClass.ToString()}") - .WriteComment($" Namespace {model.Namespace}") - .WriteComment($" Options {model.Options.ToString()}") - .WriteComment($" Type {model.Type}") - .WriteComment($" TypeIdentifierName {model.TypeIdentifierName}") - .WriteComment($" SourceNamespace {model.SourceNamespace}") - .WriteComment($" SourceTypeFullName {model.SourceTypeFullName}") - .WriteComment($" SourceTypeIdentifierName {model.SourceTypeIdentifierName}"); - - } - - internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray mappedProperties) - { - foreach (var item in mappedProperties) - { - builder .WriteComment($" Name {item.Name}") - .WriteComment($" Type {item.Type}") - .WriteComment($" MappedSourcePropertyTypeName {item.MappedSourcePropertyTypeName}") - .WriteComment($" IsEnumerable {item.IsEnumerable}") - .WriteComment($" FullyQualifiedType {item.FullyQualifiedType}") - .WriteComment($" EnumerableTypeArgument {item.EnumerableTypeArgument}") - .WriteComment($" SourcePropertyName {item.SourcePropertyName}") - .WriteComment($" TypeSymbol {item.FullyQualifiedType.ToString()}") - .WriteComment($" isReadOnly {item.isReadOnly.ToString()}") - .WriteLine(); - } - - return builder; - } - - } -} +using MapTo.Sources; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MapTo.Extensions +{ + internal static class CommonExtensions + { + internal static SourceBuilder WriteComment(this SourceBuilder builder, string comment = "") + { + return builder.WriteLine($"// {comment}"); + } + + internal static SourceBuilder WriteCommentArray(this SourceBuilder builder, IEnumerable enumerable, string name = "") + { + builder.WriteComment($"Printing Array: {name}"); + foreach (var o in enumerable) + { + if (o != null) + { + builder.WriteComment($" {o.ToString()}"); + } + } + builder.WriteComment($"End printing Array: {name}"); + + return builder; + } + + internal static SourceBuilder WriteModelInfo(this SourceBuilder builder, MappingModel model) + { + return builder + .WriteLine() + .WriteComment($" IsTypeUpdatable {model.IsTypeUpdatable}") + .WriteComment($" HasMappedBaseClass {model.HasMappedBaseClass.ToString()}") + .WriteComment($" Namespace {model.Namespace}") + .WriteComment($" Options {model.Options.ToString()}") + .WriteComment($" Type {model.Type}") + .WriteComment($" TypeIdentifierName {model.TypeIdentifierName}") + .WriteComment($" SourceNamespace {model.SourceNamespace}") + .WriteComment($" SourceTypeFullName {model.SourceTypeFullName}") + .WriteComment($" SourceTypeIdentifierName {model.SourceTypeIdentifierName}"); + + } + + internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray mappedProperties) + { + foreach (var item in mappedProperties) + { + string str = ""; + + if (item.NamedTypeSymbol != null) + foreach (var named in item.NamedTypeSymbol?.TypeArguments) + { + str += $"typeToString: {named.ToString()} "; + bool? containedTypeIsJsonEXtension = named?.HasAttribute(MappingContext.JsonExtensionAttributeSymbol); + str += $"typeArgumentTypeIsJsonExtensioN: {containedTypeIsJsonEXtension.ToString()}"; + } + + builder .WriteComment($" Name {item.Name}") + .WriteComment($" Type {item.Type}") + .WriteComment($" MappedSourcePropertyTypeName {item.MappedSourcePropertyTypeName}") + .WriteComment($" IsEnumerable {item.IsEnumerable}") + .WriteComment($" FullyQualifiedType {item.FullyQualifiedType}") + .WriteComment($" EnumerableTypeArgument {item.EnumerableTypeArgument}") + .WriteComment($" SourcePropertyName {item.SourcePropertyName}") + .WriteComment($" TypeSymbol {item.FullyQualifiedType.ToString()}") + .WriteComment($" isReadOnly {item.isReadOnly.ToString()}") + .WriteComment($" isEnumerable {item.isEnumerable.ToString()}") + .WriteComment($" INamedTypeSymbol {item.NamedTypeSymbol?.ToString()}") + .WriteComment($" INamedTypeSymbolTypeArguments {str}") + + .WriteLine(); + } + + return builder; + } + + } +} diff --git a/src/MapTo/Extensions/CommonSource.cs b/src/MapTo/Extensions/CommonSource.cs index 5ae7c1d..07f0413 100644 --- a/src/MapTo/Extensions/CommonSource.cs +++ b/src/MapTo/Extensions/CommonSource.cs @@ -1,182 +1,303 @@ -using MapTo.Sources; -using static MapTo.Sources.Constants; -using System; -using System.Collections.Generic; -using System.Text; -using System.Collections.Immutable; - -namespace MapTo.Extensions -{ - internal static class CommonSource - { - internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass) - { - const bool writeDebugInfo = true; - - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) - .WriteUsings(model.Usings) - .WriteLine() - - // Namespace declaration - .WriteLine($"namespace {model.Namespace}") - .WriteOpeningBracket(); - - if(writeDebugInfo) - builder - .WriteModelInfo(model) - .WriteLine() - .WriteComment("Type properties") - .WriteComment() - .WriteMappedProperties(model.TypeProperties) - .WriteLine() - .WriteComment("Source properties") - .WriteLine() - .WriteComment("Type fields") - .WriteComment() - .WriteMappedProperties(model.TypeFields) - .WriteLine() - .WriteComment("Source fields") - .WriteMappedProperties(model.SourceFields) - .WriteLine(); - - builder - // Class declaration - .WriteLine($"partial {structOrClass} {model.TypeIdentifierName}") - .WriteOpeningBracket() - .WriteLine() - // Class body - .GeneratePublicConstructor(model); - - if (model.IsTypeUpdatable && model.TypeProperties.GetWritableMappedProperties().Length > 0) builder.GenerateUpdateMethod(model); - - builder - .WriteLine() - // End class declaration - .WriteClosingBracket() - .WriteLine() - // End namespace declaration - .WriteClosingBracket(); - - return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); - } - - private static SourceBuilder GeneratePublicConstructor(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - const string mappingContextParameterName = "context"; - - var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty; - - var stringBuilder = new StringBuilder(); - - var otherProperties = new List(); - - foreach (var property in model.TypeProperties) - { - if (!model.SourceProperties.IsMappedProperty(property)) - { - stringBuilder.Append(", "); - stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}"); - otherProperties.Add(property); - } - } - - foreach (var property in model.TypeFields) - { - if (!model.SourceFields.IsMappedProperty(property)) - { - stringBuilder.Append(", "); - stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}"); - otherProperties.Add(property); - } - } - - - var readOnlyPropertiesArguments = stringBuilder.ToString(); - - builder - .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}") - .WriteOpeningBracket() - .WriteAssignmentMethod(model, otherProperties.ToArray().ToImmutableArray(), sourceClassParameterName, mappingContextParameterName, false); - - // End constructor declaration - return builder.WriteClosingBracket(); - } - - private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray properties, MappedMember property) { - - foreach(var prop in properties) - { - if (prop.Name == property.Name) return true; - } - - return false; - } - - private static SourceBuilder WriteAssignmentMethod(this SourceBuilder builder, MappingModel model, System.Collections.Immutable.ImmutableArray? otherProperties, - string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate) - { - - foreach (var property in model.SourceProperties) - { - if (property.isReadOnly && fromUpdate) continue; - - builder.WriteLine( $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"); - - } - - foreach (var property in model.SourceFields) - { - if (property.isReadOnly && fromUpdate) continue; - - builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"); - - } - - if (otherProperties == null) return builder; - - foreach (var property in otherProperties) - { - builder.WriteLine(property.MappedSourcePropertyTypeName is null - ? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};" - : ""); - - } - - return builder; - - } - - - private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - builder - .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName) - .WriteLine($"public void Update({model.SourceType} {sourceClassParameterName})") - .WriteOpeningBracket() - .WriteAssignmentMethod(model, null, sourceClassParameterName, "context", true) - .WriteClosingBracket(); - - return builder; - } - - private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) - { - if (!model.Options.GenerateXmlDocument) - { - return builder; - } - - return builder - .WriteLine("/// ") - .WriteLine($"/// Updates and sets its participating properties") - .WriteLine($"/// using the property values from .") - .WriteLine("/// ") - .WriteLine($"/// The instance of to use as source."); - } - } -} +using MapTo.Sources; +using static MapTo.Sources.Constants; +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis; + +namespace MapTo.Extensions +{ + internal static class CommonSource + { + internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass) + { + const bool writeDebugInfo = true; + + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) + .WriteUsings(model.Usings) + .WriteLine() + + // Namespace declaration + .WriteLine($"namespace {model.Namespace}") + .WriteOpeningBracket(); + + if (writeDebugInfo) + builder + .WriteModelInfo(model) + .WriteLine() + .WriteComment("Type properties") + .WriteComment() + .WriteMappedProperties(model.TypeProperties) + .WriteLine() + .WriteComment("Source properties") + .WriteLine() + .WriteComment("Type fields") + .WriteComment() + .WriteMappedProperties(model.TypeFields) + .WriteLine() + .WriteComment("Source fields") + .WriteMappedProperties(model.SourceFields) + .WriteLine(); + + builder + // Class declaration + .WriteLine($"partial {structOrClass} {model.TypeIdentifierName}") + .WriteOpeningBracket() + .WriteLine() + // Class body + .GeneratePublicConstructor(model); + + if (model.IsJsonExtension) builder.WriteToJsonMethod(model); + if (model.IsTypeUpdatable && model.TypeProperties.GetWritableMappedProperties().Length > 0) builder.GenerateUpdateMethod(model); + if (model.IsTypeUpdatable && model.TypeFields.GetWritableMappedProperties().Length > 0) builder.GenerateUpdateMethod(model); + + builder + .WriteLine() + // End class declaration + .WriteClosingBracket() + .WriteLine() + // End namespace declaration + .WriteClosingBracket(); + + return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); + } + + private static SourceBuilder GeneratePublicConstructor(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + const string mappingContextParameterName = "context"; + + var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty; + + var stringBuilder = new StringBuilder(); + + var otherProperties = new List(); + + foreach (var property in model.TypeProperties) + { + if (!model.SourceProperties.IsMappedProperty(property)) + { + stringBuilder.Append(", "); + stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}"); + otherProperties.Add(property); + } + } + + foreach (var property in model.TypeFields) + { + if (!model.SourceFields.IsMappedProperty(property)) + { + stringBuilder.Append(", "); + stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}"); + otherProperties.Add(property); + } + } + + + var readOnlyPropertiesArguments = stringBuilder.ToString(); + + builder + .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}") + .WriteOpeningBracket() + .WriteAssignmentMethod(model, otherProperties.ToArray().ToImmutableArray(), sourceClassParameterName, mappingContextParameterName, false); + + // End constructor declaration + return builder.WriteClosingBracket(); + } + + private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray properties, MappedMember property) + { + + foreach (var prop in properties) + { + if (prop.Name == property.Name) return true; + } + + return false; + } + + private static SourceBuilder WriteToJsonMethod(this SourceBuilder builder, MappingModel model) + { + builder + .WriteLine($"public string ToJson()") + .WriteOpeningBracket() + .WriteLine("var stringBuilder = new System.Text.StringBuilder();") + .WriteLine(GetStringBuilderAppendNoInterpolation("{")); + + foreach (var property in model.TypeProperties) + { + if (!property.isEnumerable) + HandlePropertyEnumerable(builder, property); + else + { + builder = WriteJsonField(builder, property); + } + } + foreach (var property in model.TypeFields) + { + if (!property.isEnumerable) + HandleFieldEnumerable(builder, property); + else + { + builder.WriteLine(GetStringBuilderAppend($"\\\"{property.Name.ToCamelCase()}\\\" : [{GetJsonArrayValue(property, ref builder)}],")); + } + } + + builder.WriteLine(GetStringBuilderAppendNoInterpolation("}")); + builder.WriteLine("return stringBuilder.ToString();"); + builder.WriteClosingBracket(); + return builder; + } + + private static SourceBuilder WriteJsonField(SourceBuilder builder, MappedMember property) + { + builder.WriteLine( + GetStringBuilderAppend( + $"\\\"{property.Name.ToCamelCase()}\\\" : [{GetJsonArrayValue(property, ref builder)}],")); + return builder; + } + + private static void HandleEnumerable(SourceBuilder builder, MappedMember property) + { + var symbol = property.ActualSymbol as IPropertySymbol; + builder.WriteCommentArray(symbol.Parameters, nameof(symbol.Parameters)); + builder.WriteCommentArray(symbol.TypeCustomModifiers, nameof(symbol.TypeCustomModifiers)); + + builder.WriteComment($"Is enumerable {(property.ActualSymbol as IPropertySymbol).Parameters}"); + builder.WriteLine( + GetStringBuilderAppend($"\\\"{property.Name.ToCamelCase()}\\\" : {GetJsonValue(property, builder)},")); + } + + + private static void HandleFieldEnumerable(SourceBuilder builder, MappedMember property) + { + HandleEnumerable(builder, property); + } + + private static void HandlePropertyEnumerable(SourceBuilder builder, MappedMember property) + { + HandleEnumerable(builder, property); + } + + private static string GetJsonArrayValue(MappedMember member, ref SourceBuilder builder) + { + if (member.isEnumerable) + { + // get underlying type (check if is a json extension) + + builder.WriteLine("var arrStrBuilder = new StringBuilder();"); + + foreach (var named in member.NamedTypeSymbol?.TypeArguments!) + { + bool? containedTypeIsJsonEXtension = named?.HasAttribute(MappingContext.JsonExtensionAttributeSymbol); + if (!containedTypeIsJsonEXtension.HasValue) continue; + builder.WriteLine($"foreach (var v in {member.SourcePropertyName.ToString()})"); + builder.WriteOpeningBracket(); + builder.WriteLine("arrStrBuilder.Append(v.ToJson());"); + builder.WriteLine("arrStrBuilder.Append(\", \");"); + builder.WriteClosingBracket(); + } + builder.WriteLine("arrStrBuilder.Remove(arrStrBuilder.Length -1, 1);"); + } + + return "{arrStrBuilder.ToString()}"; + } + private static string GetJsonValue(MappedMember member, SourceBuilder builder) + { + + if (member.FullyQualifiedType == "string") return $"\\\"{{{member.SourcePropertyName}}}\\\""; + if (member.FullyQualifiedType is "int" or "double" or "float" or "long") return $"{{{member.SourcePropertyName}}}"; + + return ""; + } + + private static string GetStringBuilderAppend(string stringToAppend) + { + return $"stringBuilder.Append($\"{stringToAppend}\");"; + } + private static string GetStringBuilderAppendNoInterpolation(string stringToAppend) + { + return $"stringBuilder.Append(\"{stringToAppend}\");"; + } + + private static SourceBuilder WriteAssignmentMethod(this SourceBuilder builder, MappingModel model, System.Collections.Immutable.ImmutableArray? otherProperties, + string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate) + { + + foreach (var property in model.SourceProperties) + { + if (property.isReadOnly && fromUpdate) continue; + + builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"); + + } + + foreach (var property in model.SourceFields) + { + if (property.isReadOnly && fromUpdate) continue; + + builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"); + + } + + if (otherProperties == null) return builder; + + foreach (var property in otherProperties) + { + builder.WriteLine(property.MappedSourcePropertyTypeName is null + ? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};" + : ""); + + } + + return builder; + + } + + + private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + + builder + .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName) + .WriteLine($"public void Update({model.SourceType} {sourceClassParameterName})") + .WriteOpeningBracket() + .WriteAssignmentMethod(model, null, sourceClassParameterName, "context", true) + .WriteClosingBracket(); + + return builder; + } + + private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) + { + if (!model.Options.GenerateXmlDocument) + { + return builder; + } + + return builder + .WriteLine("/// ") + .WriteLine($"/// Updates and sets its participating properties") + .WriteLine($"/// using the property values from .") + .WriteLine("/// ") + .WriteLine($"/// The instance of to use as source."); + } + + private static SourceBuilder GenerateEnumerableJsonSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + + return builder + .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") + .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static string ToJson(this IEnumerable<{model.SourceType}{model.Options.NullableReferenceSyntax}> {sourceClassParameterName}List)") + .WriteOpeningBracket() + .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});") + .WriteClosingBracket(); + } + } +} diff --git a/src/MapTo/Extensions/EnumExtensions.cs b/src/MapTo/Extensions/EnumExtensions.cs index f170a05..bf9120f 100644 --- a/src/MapTo/Extensions/EnumExtensions.cs +++ b/src/MapTo/Extensions/EnumExtensions.cs @@ -1,9 +1,9 @@ -using System; - -namespace MapTo.Extensions -{ - internal static class EnumExtensions - { - internal static string ToLowercaseString(this Enum member) => member.ToString().ToLower(); - } +using System; + +namespace MapTo.Extensions +{ + internal static class EnumExtensions + { + internal static string ToLowercaseString(this Enum member) => member.ToString().ToLower(); + } } \ No newline at end of file diff --git a/src/MapTo/Extensions/EnumerableExtensions.cs b/src/MapTo/Extensions/EnumerableExtensions.cs index 7823e58..60b0f18 100644 --- a/src/MapTo/Extensions/EnumerableExtensions.cs +++ b/src/MapTo/Extensions/EnumerableExtensions.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MapTo.Extensions -{ - internal static class EnumerableExtensions - { - internal static void ForEach(this IEnumerable enumerable, Action action) - { - foreach (var item in enumerable) - { - action(item); - } - } - - internal static bool IsEmpty(this IEnumerable enumerable) => !enumerable.Any(); - } +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MapTo.Extensions +{ + internal static class EnumerableExtensions + { + internal static void ForEach(this IEnumerable enumerable, Action action) + { + foreach (var item in enumerable) + { + action(item); + } + } + + internal static bool IsEmpty(this IEnumerable enumerable) => !enumerable.Any(); + } } \ No newline at end of file diff --git a/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs b/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs index 4b85fc8..266fb1b 100644 --- a/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs +++ b/src/MapTo/Extensions/GeneratorExecutionContextExtensions.cs @@ -1,51 +1,51 @@ -using System; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; - -namespace MapTo.Extensions -{ - internal static class GeneratorExecutionContextExtensions - { - private const string PropertyNameSuffix = "MapTo_"; - - internal static T GetBuildGlobalOption(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T : notnull - { - if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue) || string.IsNullOrWhiteSpace(optionValue)) - { - return defaultValue; - } - - var type = typeof(T); - - if (!type.IsEnum) - { - return (T)Convert.ChangeType(optionValue, type); - } - - try - { - return (T)Enum.Parse(type, optionValue, true); - } - catch (Exception) - { - context.ReportDiagnostic(DiagnosticsFactory.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property.")); - return defaultValue; - } - } - - internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}"; - - internal static Compilation AddSource(this Compilation compilation, ref GeneratorExecutionContext context, SourceCode sourceCode) - { - var sourceText = SourceText.From(sourceCode.Text, Encoding.UTF8); - context.AddSource(sourceCode.HintName, sourceText); - - // NB: https://github.com/dotnet/roslyn/issues/49753 - // To be replaced after above issue is resolved. - var options = (CSharpParseOptions)((CSharpCompilation)compilation).SyntaxTrees[0].Options; - return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceText, options)); - } - } +using System; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace MapTo.Extensions +{ + internal static class GeneratorExecutionContextExtensions + { + private const string PropertyNameSuffix = "MapTo_"; + + internal static T GetBuildGlobalOption(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T : notnull + { + if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue) || string.IsNullOrWhiteSpace(optionValue)) + { + return defaultValue; + } + + var type = typeof(T); + + if (!type.IsEnum) + { + return (T)Convert.ChangeType(optionValue, type); + } + + try + { + return (T)Enum.Parse(type, optionValue, true); + } + catch (Exception) + { + context.ReportDiagnostic(DiagnosticsFactory.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property.")); + return defaultValue; + } + } + + internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}"; + + internal static Compilation AddSource(this Compilation compilation, ref GeneratorExecutionContext context, SourceCode sourceCode) + { + var sourceText = SourceText.From(sourceCode.Text, Encoding.UTF8); + context.AddSource(sourceCode.HintName, sourceText); + + // NB: https://github.com/dotnet/roslyn/issues/49753 + // To be replaced after above issue is resolved. + var options = (CSharpParseOptions)((CSharpCompilation)compilation).SyntaxTrees[0].Options; + return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceText, options)); + } + } } \ No newline at end of file diff --git a/src/MapTo/Extensions/RoslynExtensions.cs b/src/MapTo/Extensions/RoslynExtensions.cs index c08f60a..402996e 100644 --- a/src/MapTo/Extensions/RoslynExtensions.cs +++ b/src/MapTo/Extensions/RoslynExtensions.cs @@ -1,135 +1,135 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo.Extensions -{ - internal static class RoslynExtensions - { - public static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type) - { - var current = type; - while (current != null) - { - yield return current; - - current = current.BaseType; - } - } - - public static IEnumerable GetAllMembers(this ITypeSymbol type, bool includeBaseTypeMembers = true) - { - return includeBaseTypeMembers - ? type.GetBaseTypesAndThis().SelectMany(t => t.GetMembers()) - : type.GetMembers(); - } - - public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType().Single(); - - public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text; - - public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax typeDeclarationSyntax, string attributeName) - { - return typeDeclarationSyntax.AttributeLists - .SelectMany(al => al.Attributes) - .SingleOrDefault(a => - (a.Name as IdentifierNameSyntax)?.Identifier.ValueText == attributeName || - ((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName); - } - - public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => - symbol.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true); - - public static IEnumerable GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) => - symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true); - - public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => - symbol.GetAttributes(attributeSymbol).FirstOrDefault(); - - public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax - .Ancestors() - .OfType() - .FirstOrDefault() - ?.Name - .ToString(); - - public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) => - source.TryGetTypeSymbol(out var sourceType) && destination.TryGetTypeSymbol(out var destinationType) && - (SymbolEqualityComparer.Default.Equals(destinationType, sourceType) || compilation.HasImplicitConversion(sourceType, destinationType)); - - public static bool TryGetTypeSymbol(this ISymbol symbol, [NotNullWhen(true)] out ITypeSymbol? typeSymbol) - { - switch (symbol) - { - case IPropertySymbol propertySymbol: - typeSymbol = propertySymbol.Type; - return true; - - case IFieldSymbol fieldSymbol: - typeSymbol = fieldSymbol.Type; - return true; - - case IParameterSymbol parameterSymbol: - typeSymbol = parameterSymbol.Type; - return true; - - default: - typeSymbol = null; - return false; - } - } - - public static ITypeSymbol? GetTypeSymbol(this ISymbol symbol) => symbol.TryGetTypeSymbol(out var typeSymbol) ? typeSymbol : null; - - public static IPropertySymbol? FindProperty(this IEnumerable properties, IPropertySymbol targetProperty) - { - return properties.SingleOrDefault(p => - p.Name == targetProperty.Name && - (p.NullableAnnotation != NullableAnnotation.Annotated || - p.NullableAnnotation == NullableAnnotation.Annotated && - targetProperty.NullableAnnotation == NullableAnnotation.Annotated)); - } - - public static INamedTypeSymbol GetTypeByMetadataNameOrThrow(this Compilation compilation, string fullyQualifiedMetadataName) => - compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? throw new TypeLoadException($"Unable to find '{fullyQualifiedMetadataName}' type."); - - public static bool IsGenericEnumerable(this Compilation compilation, ITypeSymbol typeSymbol) => - typeSymbol is INamedTypeSymbol { IsGenericType: true } && - compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).Equals(typeSymbol.OriginalDefinition, SymbolEqualityComparer.Default); - - public static bool IsArray(this Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol is IArrayTypeSymbol; - - public static bool IsPrimitiveType(this ITypeSymbol type) => type.SpecialType is - SpecialType.System_String or - SpecialType.System_Boolean or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_Int32 or - SpecialType.System_Int64 or - SpecialType.System_Byte or - SpecialType.System_UInt16 or - SpecialType.System_UInt32 or - SpecialType.System_UInt64 or - SpecialType.System_Single or - SpecialType.System_Double or - SpecialType.System_Char or - SpecialType.System_Object; - - public static SyntaxNode? GetSyntaxNode(this ISymbol symbol) => - symbol.Locations.FirstOrDefault() is { } location ? location.SourceTree?.GetRoot().FindNode(location.SourceSpan) : null; - - public static IEnumerable GetTypesByMetadataName(this Compilation compilation, string typeMetadataName) - { - return compilation.References - .Select(compilation.GetAssemblyOrModuleSymbol) - .OfType() - .Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName)) - .Where(t => t != null)!; - } - - public static bool TypeByMetadataNameExists(this Compilation compilation, string typeMetadataName) => GetTypesByMetadataName(compilation, typeMetadataName).Any(); - } +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo.Extensions +{ + internal static class RoslynExtensions + { + public static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type) + { + var current = type; + while (current != null) + { + yield return current; + + current = current.BaseType; + } + } + + public static IEnumerable GetAllMembers(this ITypeSymbol type, bool includeBaseTypeMembers = true) + { + return includeBaseTypeMembers + ? type.GetBaseTypesAndThis().SelectMany(t => t.GetMembers()) + : type.GetMembers(); + } + + public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType().Single(); + + public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text; + + public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax typeDeclarationSyntax, string attributeName) + { + return typeDeclarationSyntax.AttributeLists + .SelectMany(al => al.Attributes) + .SingleOrDefault(a => + (a.Name as IdentifierNameSyntax)?.Identifier.ValueText == attributeName || + ((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName); + } + + public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => + symbol.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true); + + public static IEnumerable GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) => + symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true); + + public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => + symbol.GetAttributes(attributeSymbol).FirstOrDefault(); + + public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax + .Ancestors() + .OfType() + .FirstOrDefault() + ?.Name + .ToString(); + + public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) => + source.TryGetTypeSymbol(out var sourceType) && destination.TryGetTypeSymbol(out var destinationType) && + (SymbolEqualityComparer.Default.Equals(destinationType, sourceType) || compilation.HasImplicitConversion(sourceType, destinationType)); + + public static bool TryGetTypeSymbol(this ISymbol symbol, [NotNullWhen(true)] out ITypeSymbol? typeSymbol) + { + switch (symbol) + { + case IPropertySymbol propertySymbol: + typeSymbol = propertySymbol.Type; + return true; + + case IFieldSymbol fieldSymbol: + typeSymbol = fieldSymbol.Type; + return true; + + case IParameterSymbol parameterSymbol: + typeSymbol = parameterSymbol.Type; + return true; + + default: + typeSymbol = null; + return false; + } + } + + public static ITypeSymbol? GetTypeSymbol(this ISymbol symbol) => symbol.TryGetTypeSymbol(out var typeSymbol) ? typeSymbol : null; + + public static IPropertySymbol? FindProperty(this IEnumerable properties, IPropertySymbol targetProperty) + { + return properties.SingleOrDefault(p => + p.Name == targetProperty.Name && + (p.NullableAnnotation != NullableAnnotation.Annotated || + p.NullableAnnotation == NullableAnnotation.Annotated && + targetProperty.NullableAnnotation == NullableAnnotation.Annotated)); + } + + public static INamedTypeSymbol GetTypeByMetadataNameOrThrow(this Compilation compilation, string fullyQualifiedMetadataName) => + compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? throw new TypeLoadException($"Unable to find '{fullyQualifiedMetadataName}' type."); + + public static bool IsGenericEnumerable(this Compilation compilation, ITypeSymbol typeSymbol) => + typeSymbol is INamedTypeSymbol { IsGenericType: true } && + compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).Equals(typeSymbol.OriginalDefinition, SymbolEqualityComparer.Default); + + public static bool IsArray(this Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol is IArrayTypeSymbol; + + public static bool IsPrimitiveType(this ITypeSymbol type) => type.SpecialType is + SpecialType.System_String or + SpecialType.System_Boolean or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_Int32 or + SpecialType.System_Int64 or + SpecialType.System_Byte or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_UInt64 or + SpecialType.System_Single or + SpecialType.System_Double or + SpecialType.System_Char or + SpecialType.System_Object; + + public static SyntaxNode? GetSyntaxNode(this ISymbol symbol) => + symbol.Locations.FirstOrDefault() is { } location ? location.SourceTree?.GetRoot().FindNode(location.SourceSpan) : null; + + public static IEnumerable GetTypesByMetadataName(this Compilation compilation, string typeMetadataName) + { + return compilation.References + .Select(compilation.GetAssemblyOrModuleSymbol) + .OfType() + .Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName)) + .Where(t => t != null)!; + } + + public static bool TypeByMetadataNameExists(this Compilation compilation, string typeMetadataName) => GetTypesByMetadataName(compilation, typeMetadataName).Any(); + } } \ No newline at end of file diff --git a/src/MapTo/Extensions/StringBuilderExtensions.cs b/src/MapTo/Extensions/StringBuilderExtensions.cs index 5d26819..95aa64b 100644 --- a/src/MapTo/Extensions/StringBuilderExtensions.cs +++ b/src/MapTo/Extensions/StringBuilderExtensions.cs @@ -1,30 +1,30 @@ -using System; -using System.Text; - -namespace MapTo.Extensions -{ - internal static class StringBuilderExtensions - { - public static StringBuilder PadLeft(this StringBuilder builder, int width) - { - for (var i = 0; i < width; i++) - { - builder.Append(" "); - } - - return builder; - } - - internal static StringBuilder AppendOpeningBracket(this StringBuilder builder, int indent = 0) => builder.AppendLine().PadLeft(indent).AppendFormat("{{{0}", Environment.NewLine); - - internal static StringBuilder AppendClosingBracket(this StringBuilder builder, int indent = 0, bool padNewLine = true) - { - if (padNewLine) - { - builder.AppendLine(); - } - - return builder.PadLeft(indent).Append("}"); - } - } +using System; +using System.Text; + +namespace MapTo.Extensions +{ + internal static class StringBuilderExtensions + { + public static StringBuilder PadLeft(this StringBuilder builder, int width) + { + for (var i = 0; i < width; i++) + { + builder.Append(" "); + } + + return builder; + } + + internal static StringBuilder AppendOpeningBracket(this StringBuilder builder, int indent = 0) => builder.AppendLine().PadLeft(indent).AppendFormat("{{{0}", Environment.NewLine); + + internal static StringBuilder AppendClosingBracket(this StringBuilder builder, int indent = 0, bool padNewLine = true) + { + if (padNewLine) + { + builder.AppendLine(); + } + + return builder.PadLeft(indent).Append("}"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Extensions/StringExtensions.cs b/src/MapTo/Extensions/StringExtensions.cs index 8e1437a..5579f36 100644 --- a/src/MapTo/Extensions/StringExtensions.cs +++ b/src/MapTo/Extensions/StringExtensions.cs @@ -1,17 +1,17 @@ -using System.Threading.Tasks; - -namespace MapTo.Extensions -{ - internal static class StringExtensions - { - public static string ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}"; - - public static string ToSourceCodeString(this object? value) => value switch - { - null => "null", - string strValue => $"\"{strValue}\"", - char charValue => $"'{charValue}'", - _ => value.ToString() - }; - } +using System.Threading.Tasks; + +namespace MapTo.Extensions +{ + internal static class StringExtensions + { + public static string ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}"; + + public static string ToSourceCodeString(this object? value) => value switch + { + null => "null", + string strValue => $"\"{strValue}\"", + char charValue => $"'{charValue}'", + _ => value.ToString() + }; + } } \ No newline at end of file diff --git a/src/MapTo/MapTo.csproj b/src/MapTo/MapTo.csproj index 21d8219..d5ae058 100644 --- a/src/MapTo/MapTo.csproj +++ b/src/MapTo/MapTo.csproj @@ -1,51 +1,51 @@ - - - - netstandard2.0 - enable - 9 - - MapTo - An object to object mapping generator using Roslyn source generator. - true - true - MapTo - LICENSE - https://github.com/mrtaikandi/mapto - false - $(Version) - true - https://github.com/mrtaikandi/mapto - snupkg - MapTo - - - - bin\Release\MapTo.xml - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - + + + + netstandard2.0 + enable + 9 + + MapTo + An object to object mapping generator using Roslyn source generator. + true + true + MapTo + LICENSE + https://github.com/mrtaikandi/mapto + false + $(Version) + true + https://github.com/mrtaikandi/mapto + snupkg + MapTo + + + + bin\Release\MapTo.xml + + + + + <_Parameter1>$(AssemblyName).Tests + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/src/MapTo/MapTo.props b/src/MapTo/MapTo.props index 2082ff5..4d9ec67 100644 --- a/src/MapTo/MapTo.props +++ b/src/MapTo/MapTo.props @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index 8a524a7..c9b12eb 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -1,75 +1,76 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MapTo.Extensions; -using MapTo.Sources; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo -{ - /// - /// MapTo source generator. - /// - [Generator] - public class MapToGenerator : ISourceGenerator - { - /// - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver()); - } - - /// - public void Execute(GeneratorExecutionContext context) - { - try - { - var options = SourceGenerationOptions.From(context); - - var compilation = context.Compilation - .AddSource(ref context, UseUpdateAttributeSource.Generate(options)) - .AddSource(ref context, MapFromAttributeSource.Generate(options)) - .AddSource(ref context, IgnorePropertyAttributeSource.Generate(options)) - .AddSource(ref context, ITypeConverterSource.Generate(options)) - .AddSource(ref context, MapTypeConverterAttributeSource.Generate(options)) - .AddSource(ref context, MapPropertyAttributeSource.Generate(options)) - .AddSource(ref context, MappingContextSource.Generate(options)); - - if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateTypes.Any()) - { - AddGeneratedMappingsClasses(context, compilation, receiver.CandidateTypes, options); - } - } - catch (Exception ex) - { - Console.WriteLine(ex); - throw; - } - } - - private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable candidateTypes, SourceGenerationOptions options) - { - foreach (var typeDeclarationSyntax in candidateTypes) - { - var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax); - mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); - - if (mappingContext.Model is null) - { - continue; - } - - var (source, hintName) = typeDeclarationSyntax switch - { - StructDeclarationSyntax => MapStructSource.Generate(mappingContext.Model), - ClassDeclarationSyntax => MapClassSource.Generate(mappingContext.Model), - RecordDeclarationSyntax => MapRecordSource.Generate(mappingContext.Model), - _ => throw new ArgumentOutOfRangeException() - }; - - context.AddSource(hintName, source); - } - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo +{ + /// + /// MapTo source generator. + /// + [Generator] + public class MapToGenerator : ISourceGenerator + { + /// + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver()); + } + + /// + public void Execute(GeneratorExecutionContext context) + { + try + { + var options = SourceGenerationOptions.From(context); + + var compilation = context.Compilation + .AddSource(ref context, UseUpdateAttributeSource.Generate(options)) + .AddSource(ref context, JsonExtensionAttributeSource.Generate(options)) + .AddSource(ref context, MapFromAttributeSource.Generate(options)) + .AddSource(ref context, IgnoreMemberAttributeSource.Generate(options)) + .AddSource(ref context, ITypeConverterSource.Generate(options)) + .AddSource(ref context, MapTypeConverterAttributeSource.Generate(options)) + .AddSource(ref context, MapPropertyAttributeSource.Generate(options)) + .AddSource(ref context, MappingContextSource.Generate(options)); + + if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateTypes.Any()) + { + AddGeneratedMappingsClasses(context, compilation, receiver.CandidateTypes, options); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw; + } + } + + private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable candidateTypes, SourceGenerationOptions options) + { + foreach (var typeDeclarationSyntax in candidateTypes) + { + var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax); + mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); + + if (mappingContext.Model is null) + { + continue; + } + + var (source, hintName) = typeDeclarationSyntax switch + { + StructDeclarationSyntax => MapStructSource.Generate(mappingContext.Model), + ClassDeclarationSyntax => MapClassSource.Generate(mappingContext.Model), + RecordDeclarationSyntax => MapRecordSource.Generate(mappingContext.Model), + _ => throw new ArgumentOutOfRangeException() + }; + + context.AddSource(hintName, source); + } + } + } } \ No newline at end of file diff --git a/src/MapTo/MapToSyntaxReceiver.cs b/src/MapTo/MapToSyntaxReceiver.cs index 24ce03a..03c35ac 100644 --- a/src/MapTo/MapToSyntaxReceiver.cs +++ b/src/MapTo/MapToSyntaxReceiver.cs @@ -1,39 +1,39 @@ -using System.Collections.Generic; -using System.Linq; -using MapTo.Sources; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo -{ - internal class MapToSyntaxReceiver : ISyntaxReceiver - { - public List CandidateTypes { get; } = new(); - - /// - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax) - { - return; - } - - var attributeSyntax = attributes - .SelectMany(a => a.Attributes) - .SingleOrDefault(a => a.Name is - IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom] - or - QualifiedNameSyntax // For: [MapTo.MapFrom] - { - Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } }, - Right: IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } - } - ); - - if (attributeSyntax is not null) - { - CandidateTypes.Add(typeDeclarationSyntax); - } - } - } +using System.Collections.Generic; +using System.Linq; +using MapTo.Sources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo +{ + internal class MapToSyntaxReceiver : ISyntaxReceiver + { + public List CandidateTypes { get; } = new(); + + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax) + { + return; + } + + var attributeSyntax = attributes + .SelectMany(a => a.Attributes) + .SingleOrDefault(a => a.Name is + IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom] + or + QualifiedNameSyntax // For: [MapTo.MapFrom] + { + Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } }, + Right: IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } + } + ); + + if (attributeSyntax is not null) + { + CandidateTypes.Add(typeDeclarationSyntax); + } + } + } } \ No newline at end of file diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index d884bcf..74fe87d 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -1,541 +1,592 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using MapTo.Extensions; -using MapTo.Sources; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo -{ - internal static class MappingContextExtensions - { - internal static ImmutableArray GetReadOnlyMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!; - internal static ImmutableArray GetWritableMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!; - } - - internal abstract class MappingContext - { - private readonly List _ignoredNamespaces; - - protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) - { - _ignoredNamespaces = new(); - Diagnostics = ImmutableArray.Empty; - Usings = ImmutableArray.Create("System", Constants.RootNamespace); - SourceGenerationOptions = sourceGenerationOptions; - TypeSyntax = typeSyntax; - Compilation = compilation; - - IgnorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName); - MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName); - TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName); - MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName); - MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName); - UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName); - MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName); - - AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis"); - } - - public ImmutableArray Diagnostics { get; private set; } - - public MappingModel? Model { get; private set; } - - protected Compilation Compilation { get; } - - protected INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; } - - protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; } - - protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; } - - protected INamedTypeSymbol MappingContextTypeSymbol { get; } - - protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; } - - protected INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; } - - protected SourceGenerationOptions SourceGenerationOptions { get; } - - protected INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; } - - protected TypeDeclarationSyntax TypeSyntax { get; } - - protected ImmutableArray Usings { get; private set; } - - public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) - { - MappingContext context = typeSyntax switch - { - StructDeclarationSyntax => new StructMappingContext(compilation, sourceGenerationOptions, typeSyntax), - ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax), - RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax), - _ => throw new ArgumentOutOfRangeException() - }; - - context.Model = context.CreateMappingModel(); - - return context; - } - - protected void AddDiagnostic(Diagnostic diagnostic) - { - Diagnostics = Diagnostics.Add(diagnostic); - } - - protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) => - AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace); - - protected void AddUsingIfRequired(bool condition, INamespaceSymbol? ns) => - AddUsingIfRequired(condition && ns is not null && !_ignoredNamespaces.Contains(ns.ToDisplayParts().First()), ns?.ToDisplayString()); - - protected void AddUsingIfRequired(bool condition, string? ns) - { - if (ns is not null && condition && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns)) - { - Usings = Usings.Add(ns); - } - } - - protected IPropertySymbol? FindSourceProperty(IEnumerable sourceProperties, ISymbol property) - { - var propertyName = property - .GetAttribute(MapPropertyAttributeTypeSymbol) - ?.NamedArguments - .SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName) - .Value.Value as string ?? property.Name; - - return sourceProperties.SingleOrDefault(p => p.Name == propertyName); - } - protected IFieldSymbol? FindSourceField(IEnumerable sourceProperties, ISymbol property) - { - var propertyName = property - .GetAttribute(MapPropertyAttributeTypeSymbol) - ?.NamedArguments - .SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName) - .Value.Value as string ?? property.Name; - - return sourceProperties.SingleOrDefault(p => p.Name == propertyName); - } - - protected abstract ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); - protected abstract ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); - - - protected abstract ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); - protected abstract ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); - - - protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) => - GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel); - - protected INamedTypeSymbol? GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null) - { - if (attributeSyntax is null) - { - return null; - } - - semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree); - var sourceTypeExpressionSyntax = attributeSyntax - .DescendantNodes() - .OfType() - .SingleOrDefault(); - - return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; - } - - protected bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel) - { - return TypeSyntax.BaseList is not null && TypeSyntax.BaseList.Types - .Select(t => semanticModel.GetTypeInfo(t.Type).Type) - .Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null); - } - - protected bool IsTypeUpdatable() - { - return TypeSyntax.GetAttribute("UseUpdate") != null; - } - - protected virtual MappedMember? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection sourceProperties, ISymbol property) - { - var sourceProperty = FindSourceProperty(sourceProperties, property); - if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType)) - { - return null; - } - - - string? converterFullyQualifiedName = null; - var converterParameters = ImmutableArray.Empty; - ITypeSymbol? mappedSourcePropertyType = null; - ITypeSymbol? enumerableTypeArgumentType = null; - - if (!Compilation.HasCompatibleTypes(sourceProperty, property)) - { - if (!TryGetMapTypeConverterForProperty(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) && - !TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType)) - { - return null; - } - } - - AddUsingIfRequired(propertyType); - AddUsingIfRequired(enumerableTypeArgumentType); - AddUsingIfRequired(mappedSourcePropertyType); - - - return new MappedMember( - property.Name, - property.GetTypeSymbol().ToString(), - ToQualifiedDisplayName(propertyType) ?? propertyType.Name, - converterFullyQualifiedName, - converterParameters.ToImmutableArray(), - sourceProperty.Name, - ToQualifiedDisplayName(mappedSourcePropertyType), - ToQualifiedDisplayName(enumerableTypeArgumentType), - (property as IPropertySymbol).IsReadOnly); -; - } - - protected virtual MappedMember? MapField(ISymbol sourceTypeSymbol, IReadOnlyCollection sourceProperties, ISymbol property) - { - var sourceProperty = FindSourceField(sourceProperties, property); - if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType)) - { - return null; - } - - if (property is IFieldSymbol symbol) - { - if (symbol.AssociatedSymbol != null) return null; - } - - string? converterFullyQualifiedName = null; - var converterParameters = ImmutableArray.Empty; - ITypeSymbol? mappedSourcePropertyType = null; - ITypeSymbol? enumerableTypeArgumentType = null; - - if (!Compilation.HasCompatibleTypes(sourceProperty, property)) - { - if (!TryGetMapTypeConverterForField(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) && - !TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType)) - { - return null; - } - } - - AddUsingIfRequired(propertyType); - AddUsingIfRequired(enumerableTypeArgumentType); - AddUsingIfRequired(mappedSourcePropertyType); - - - return new MappedMember( - property.Name, - property.GetTypeSymbol().ToString(), - ToQualifiedDisplayName(propertyType) ?? propertyType.Name, - converterFullyQualifiedName, - converterParameters.ToImmutableArray(), - sourceProperty.Name, - ToQualifiedDisplayName(mappedSourcePropertyType), - ToQualifiedDisplayName(enumerableTypeArgumentType), - (property as IFieldSymbol).IsReadOnly); - ; - } - protected virtual MappedMember? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property) - { - if (!property.TryGetTypeSymbol(out var propertyType)) - { - return null; - } - - - string? converterFullyQualifiedName = null; - var converterParameters = ImmutableArray.Empty; - ITypeSymbol? mappedSourcePropertyType = null; - ITypeSymbol? enumerableTypeArgumentType = null; - - - AddUsingIfRequired(propertyType); - AddUsingIfRequired(enumerableTypeArgumentType); - AddUsingIfRequired(mappedSourcePropertyType); - - - return new MappedMember( - property.Name, - property.GetTypeSymbol().ToString(), - ToQualifiedDisplayName(propertyType) ?? propertyType.Name, - converterFullyQualifiedName, - converterParameters.ToImmutableArray(), - property.Name, - ToQualifiedDisplayName(mappedSourcePropertyType), - ToQualifiedDisplayName(enumerableTypeArgumentType), - (property as IPropertySymbol).IsReadOnly); - ; - } - - protected virtual MappedMember? MapFieldSimple(ISymbol sourceTypeSymbol, ISymbol property) - { - if (!property.TryGetTypeSymbol(out var propertyType)) - { - return null; - } - - if(property is IFieldSymbol symbol) - { - if (symbol.AssociatedSymbol != null) return null; - } - - - string? converterFullyQualifiedName = null; - var converterParameters = ImmutableArray.Empty; - ITypeSymbol? mappedSourcePropertyType = null; - ITypeSymbol? enumerableTypeArgumentType = null; - - - AddUsingIfRequired(propertyType); - AddUsingIfRequired(enumerableTypeArgumentType); - AddUsingIfRequired(mappedSourcePropertyType); - - - return new MappedMember( - property.Name, - property.GetTypeSymbol().ToString(), - ToQualifiedDisplayName(propertyType) ?? propertyType.Name, - converterFullyQualifiedName, - converterParameters.ToImmutableArray(), - property.Name, - ToQualifiedDisplayName(mappedSourcePropertyType), - ToQualifiedDisplayName(enumerableTypeArgumentType), - (property as IFieldSymbol).IsReadOnly); - ; - } - protected bool TryGetMapTypeConverterForProperty(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName, - out ImmutableArray converterParameters) - { - converterFullyQualifiedName = null; - converterParameters = ImmutableArray.Empty; - - if (!Diagnostics.IsEmpty()) - { - return false; - } - - var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol); - if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol) - { - return false; - } - - var baseInterface = GetTypeConverterBaseInterfaceForProperty(converterTypeSymbol, property, sourceProperty); - if (baseInterface is null) - { - AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty)); - return false; - } - - converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); - converterParameters = GetTypeConverterParameters(typeConverterAttribute); - return true; - } - protected bool TryGetMapTypeConverterForField(ISymbol property, IFieldSymbol sourceProperty, out string? converterFullyQualifiedName, - out ImmutableArray converterParameters) - { - converterFullyQualifiedName = null; - converterParameters = ImmutableArray.Empty; - - if (!Diagnostics.IsEmpty()) - { - return false; - } - - var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol); - if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol) - { - return false; - } - - var baseInterface = GetTypeConverterBaseInterfaceForField(converterTypeSymbol, property, sourceProperty); - if (baseInterface is null) - { - //AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, null)); - return false; - } - - converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); - converterParameters = GetTypeConverterParameters(typeConverterAttribute); - return true; - } - protected bool TryGetNestedObjectMappings(ISymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument) - { - mappedSourcePropertyType = null; - enumerableTypeArgument = null; - - if (!Diagnostics.IsEmpty()) - { - return false; - } - - if (!property.TryGetTypeSymbol(out var propertyType)) - { - AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property)); - return false; - } - - var mapFromAttribute = propertyType.GetAttribute(MapFromAttributeTypeSymbol); - if (mapFromAttribute is null && - propertyType is INamedTypeSymbol namedTypeSymbol && - !propertyType.IsPrimitiveType() && - (Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i)))) - { - enumerableTypeArgument = namedTypeSymbol.TypeArguments.First(); - mapFromAttribute = enumerableTypeArgument.GetAttribute(MapFromAttributeTypeSymbol); - } - - mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol; - - if (mappedSourcePropertyType is null && enumerableTypeArgument is null) - { - AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property)); - } - - return Diagnostics.IsEmpty(); - } - - private static ImmutableArray GetTypeConverterParameters(AttributeData typeConverterAttribute) - { - var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault(); - return converterParameter.IsNull - ? ImmutableArray.Empty - : converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray(); - } - - private MappingModel? CreateMappingModel() - { - var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree); - if (semanticModel.GetDeclaredSymbol(TypeSyntax) is not INamedTypeSymbol typeSymbol) - { - AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(TypeSyntax.GetLocation(), TypeSyntax.Identifier.ValueText)); - return null; - } - - var sourceTypeSymbol = GetSourceTypeSymbol(TypeSyntax, semanticModel); - if (sourceTypeSymbol is null) - { - AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation())); - return null; - } - - _ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First()); - - var typeIdentifierName = TypeSyntax.GetIdentifierName(); - var sourceTypeIdentifierName = sourceTypeSymbol.Name; - var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel); - var isTypeUpdatable = IsTypeUpdatable(); - var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol); - - var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); - var mappedFields = GetSourceMappedFields(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); - - /*if (!mappedProperties.Any()) - { - AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol)); - return null; - }*/ - - AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq"); - - var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass); - var allFields = GetTypeMappedFields(sourceTypeSymbol, typeSymbol, isTypeInheritFromMappedBaseClass); - - return new MappingModel( - SourceGenerationOptions, - TypeSyntax.GetNamespace(), - TypeSyntax.Modifiers, - TypeSyntax.Keyword.Text, - typeIdentifierName, - sourceTypeSymbol.ContainingNamespace.ToDisplayString(), - sourceTypeIdentifierName, - sourceTypeSymbol.ToDisplayString(), - isTypeUpdatable, - mappedProperties, - allProperties, - mappedFields, - allFields, - isTypeInheritFromMappedBaseClass, - Usings, - shouldGenerateSecondaryConstructor); - } - - - - private INamedTypeSymbol? GetTypeConverterBaseInterfaceForProperty(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty) - { - if (!property.TryGetTypeSymbol(out var propertyType)) - { - return null; - } - - return converterTypeSymbol.AllInterfaces - .SingleOrDefault(i => - i.TypeArguments.Length == 2 && - SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) && - SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && - SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1])); - } - private INamedTypeSymbol? GetTypeConverterBaseInterfaceForField(ITypeSymbol converterTypeSymbol, ISymbol property, IFieldSymbol sourceProperty) - { - if (!property.TryGetTypeSymbol(out var propertyType)) - { - return null; - } - - return converterTypeSymbol.AllInterfaces - .SingleOrDefault(i => - i.TypeArguments.Length == 2 && - SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) && - SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && - SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1])); - } - - private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol) - { - var constructorSyntax = TypeSyntax.DescendantNodes() - .OfType() - .SingleOrDefault(c => - c.ParameterList.Parameters.Count == 1 && - SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(c.ParameterList.Parameters.Single().Type!).ConvertedType, sourceTypeSymbol)); - - if (constructorSyntax is null) - { - // Secondary constructor is not defined. - return true; - } - - if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments || - !SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, MappingContextTypeSymbol) || - !SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol)) - { - AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax)); - } - - return false; - } - - private string? ToQualifiedDisplayName(ISymbol? symbol) - { - if (symbol is null) - { - return null; - } - - var containingNamespace = TypeSyntax.GetNamespace(); - var symbolNamespace = symbol.ContainingNamespace.ToDisplayString(); - return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First()) - ? symbol.ToDisplayString() - : symbol.Name; - } - } +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo +{ + internal static class MappingContextExtensions + { + internal static ImmutableArray GetReadOnlyMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!; + internal static ImmutableArray GetWritableMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!; + } + + internal abstract class MappingContext + { + private readonly List _ignoredNamespaces; + + protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) + { + _ignoredNamespaces = new(); + Diagnostics = ImmutableArray.Empty; + Usings = ImmutableArray.Create("System", Constants.RootNamespace); + SourceGenerationOptions = sourceGenerationOptions; + TypeSyntax = typeSyntax; + Compilation = compilation; + + IgnoreMemberAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnoreMemberAttributeSource.FullyQualifiedName); + MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName); + TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName); + MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName); + MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName); + UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName); + JsonExtensionAttributeSymbol = compilation.GetTypeByMetadataNameOrThrow(JsonExtensionAttributeSource.FullyQualifiedName); + MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName); + + AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis"); + } + + public ImmutableArray Diagnostics { get; private set; } + + public MappingModel? Model { get; private set; } + + protected Compilation Compilation { get; } + + protected INamedTypeSymbol IgnoreMemberAttributeTypeSymbol { get; } + + protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; } + + protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; } + + public static INamedTypeSymbol JsonExtensionAttributeSymbol { get; set; } + + protected INamedTypeSymbol MappingContextTypeSymbol { get; } + + protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; } + + protected INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; } + + protected SourceGenerationOptions SourceGenerationOptions { get; } + + protected INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; } + + protected TypeDeclarationSyntax TypeSyntax { get; } + + protected ImmutableArray Usings { get; private set; } + + public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) + { + MappingContext context = typeSyntax switch + { + StructDeclarationSyntax => new StructMappingContext(compilation, sourceGenerationOptions, typeSyntax), + ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax), + RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax), + _ => throw new ArgumentOutOfRangeException() + }; + + context.Model = context.CreateMappingModel(); + + return context; + } + + protected void AddDiagnostic(Diagnostic diagnostic) + { + Diagnostics = Diagnostics.Add(diagnostic); + } + + protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) => + AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace); + + protected void AddUsingIfRequired(bool condition, INamespaceSymbol? ns) => + AddUsingIfRequired(condition && ns is not null && !_ignoredNamespaces.Contains(ns.ToDisplayParts().First()), ns?.ToDisplayString()); + + protected void AddUsingIfRequired(bool condition, string? ns) + { + if (ns is not null && condition && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns)) + { + Usings = Usings.Add(ns); + } + } + + protected IPropertySymbol? FindSourceProperty(IEnumerable sourceProperties, ISymbol property) + { + var propertyName = property + .GetAttribute(MapPropertyAttributeTypeSymbol) + ?.NamedArguments + .SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName) + .Value.Value as string ?? property.Name; + + return sourceProperties.SingleOrDefault(p => p.Name == propertyName); + } + protected IFieldSymbol? FindSourceField(IEnumerable sourceProperties, ISymbol property) + { + var propertyName = property + .GetAttribute(MapPropertyAttributeTypeSymbol) + ?.NamedArguments + .SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName) + .Value.Value as string ?? property.Name; + + return sourceProperties.SingleOrDefault(p => p.Name == propertyName); + } + + protected abstract ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); + protected abstract ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); + + + protected abstract ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); + protected abstract ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); + + + protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) => + GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel); + + protected INamedTypeSymbol? GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null) + { + if (attributeSyntax is null) + { + return null; + } + + semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree); + var sourceTypeExpressionSyntax = attributeSyntax + .DescendantNodes() + .OfType() + .SingleOrDefault(); + + return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null; + } + + protected bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel) + { + return TypeSyntax.BaseList is not null && TypeSyntax.BaseList.Types + .Select(t => semanticModel.GetTypeInfo(t.Type).Type) + .Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null); + } + + protected bool IsTypeUpdatable() + { + return TypeSyntax.GetAttribute("UseUpdate") != null; + } + protected bool HasJsonExtension() + { + return TypeSyntax.GetAttribute("JsonExtension") != null; + } + protected virtual MappedMember? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection sourceProperties, ISymbol property) + { + var sourceProperty = FindSourceProperty(sourceProperties, property); + if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType)) + { + return null; + } + + + string? converterFullyQualifiedName = null; + var converterParameters = ImmutableArray.Empty; + ITypeSymbol? mappedSourcePropertyType = null; + ITypeSymbol? enumerableTypeArgumentType = null; + + if (!Compilation.HasCompatibleTypes(sourceProperty, property)) + { + if (!TryGetMapTypeConverterForProperty(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) && + !TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType)) + { + return null; + } + } + + AddUsingIfRequired(propertyType); + AddUsingIfRequired(enumerableTypeArgumentType); + AddUsingIfRequired(mappedSourcePropertyType); + + INamedTypeSymbol? namedType; + var isEnumerable = IsEnumerable(property, out namedType); + + + return new MappedMember( + property.Name, + property.GetTypeSymbol().ToString(), + ToQualifiedDisplayName(propertyType) ?? propertyType.Name, + converterFullyQualifiedName, + converterParameters.ToImmutableArray(), + sourceProperty.Name, + ToQualifiedDisplayName(mappedSourcePropertyType), + ToQualifiedDisplayName(enumerableTypeArgumentType), + property, + namedType, + isEnumerable, + (property as IPropertySymbol).IsReadOnly); +; + } + + protected virtual MappedMember? MapField(ISymbol sourceTypeSymbol, IReadOnlyCollection sourceProperties, ISymbol property) + { + var sourceProperty = FindSourceField(sourceProperties, property); + if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType)) + { + return null; + } + + if (property is IFieldSymbol symbol) + { + if (symbol.AssociatedSymbol != null) return null; + } + + string? converterFullyQualifiedName = null; + var converterParameters = ImmutableArray.Empty; + ITypeSymbol? mappedSourcePropertyType = null; + ITypeSymbol? enumerableTypeArgumentType = null; + + if (!Compilation.HasCompatibleTypes(sourceProperty, property)) + { + if (!TryGetMapTypeConverterForField(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) && + !TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType)) + { + return null; + } + } + + AddUsingIfRequired(propertyType); + AddUsingIfRequired(enumerableTypeArgumentType); + AddUsingIfRequired(mappedSourcePropertyType); + + + INamedTypeSymbol? namedType; + var isEnumerable = IsEnumerable(property, out namedType); + + return new MappedMember( + property.Name, + property.GetTypeSymbol().ToString(), + ToQualifiedDisplayName(propertyType) ?? propertyType.Name, + converterFullyQualifiedName, + converterParameters.ToImmutableArray(), + sourceProperty.Name, + ToQualifiedDisplayName(mappedSourcePropertyType), + ToQualifiedDisplayName(enumerableTypeArgumentType), + property, + namedType, + isEnumerable, + (property as IFieldSymbol).IsReadOnly); + ; + } + protected virtual MappedMember? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property) + { + if (!property.TryGetTypeSymbol(out var propertyType)) + { + return null; + } + + + string? converterFullyQualifiedName = null; + var converterParameters = ImmutableArray.Empty; + ITypeSymbol? mappedSourcePropertyType = null; + ITypeSymbol? enumerableTypeArgumentType = null; + + + AddUsingIfRequired(propertyType); + AddUsingIfRequired(enumerableTypeArgumentType); + AddUsingIfRequired(mappedSourcePropertyType); + + INamedTypeSymbol? namedType; + var isEnumerable = IsEnumerable(property, out namedType); + + return new MappedMember( + property.Name, + property.GetTypeSymbol().ToString(), + ToQualifiedDisplayName(propertyType) ?? propertyType.Name, + converterFullyQualifiedName, + converterParameters.ToImmutableArray(), + property.Name, + ToQualifiedDisplayName(mappedSourcePropertyType), + ToQualifiedDisplayName(enumerableTypeArgumentType), + property, + namedType, + isEnumerable, + (property as IPropertySymbol).IsReadOnly); + ; + } + + protected virtual MappedMember? MapFieldSimple(ISymbol sourceTypeSymbol, ISymbol property) + { + if (!property.TryGetTypeSymbol(out var propertyType)) + { + return null; + } + + if(property is IFieldSymbol symbol) + { + if (symbol.AssociatedSymbol != null) return null; + } + + + string? converterFullyQualifiedName = null; + var converterParameters = ImmutableArray.Empty; + ITypeSymbol? mappedSourcePropertyType = null; + ITypeSymbol? enumerableTypeArgumentType = null; + + + AddUsingIfRequired(propertyType); + AddUsingIfRequired(enumerableTypeArgumentType); + AddUsingIfRequired(mappedSourcePropertyType); + + INamedTypeSymbol? namedType; + var isEnumerable = IsEnumerable(property, out namedType); + + + return new MappedMember( + property.Name, + property.GetTypeSymbol().ToString(), + ToQualifiedDisplayName(propertyType) ?? propertyType.Name, + converterFullyQualifiedName, + converterParameters.ToImmutableArray(), + property.Name, + ToQualifiedDisplayName(mappedSourcePropertyType), + ToQualifiedDisplayName(enumerableTypeArgumentType), + property, + namedType, + isEnumerable, + (property as IFieldSymbol).IsReadOnly); + ; + } + protected bool TryGetMapTypeConverterForProperty(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName, + out ImmutableArray converterParameters) + { + converterFullyQualifiedName = null; + converterParameters = ImmutableArray.Empty; + + if (!Diagnostics.IsEmpty()) + { + return false; + } + + var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol); + if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol) + { + return false; + } + + var baseInterface = GetTypeConverterBaseInterfaceForProperty(converterTypeSymbol, property, sourceProperty); + if (baseInterface is null) + { + AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty)); + return false; + } + + converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); + converterParameters = GetTypeConverterParameters(typeConverterAttribute); + return true; + } + protected bool TryGetMapTypeConverterForField(ISymbol property, IFieldSymbol sourceProperty, out string? converterFullyQualifiedName, + out ImmutableArray converterParameters) + { + converterFullyQualifiedName = null; + converterParameters = ImmutableArray.Empty; + + if (!Diagnostics.IsEmpty()) + { + return false; + } + + var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol); + if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol) + { + return false; + } + + var baseInterface = GetTypeConverterBaseInterfaceForField(converterTypeSymbol, property, sourceProperty); + if (baseInterface is null) + { + //AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, null)); + return false; + } + + converterFullyQualifiedName = converterTypeSymbol.ToDisplayString(); + converterParameters = GetTypeConverterParameters(typeConverterAttribute); + return true; + } + protected bool TryGetNestedObjectMappings(ISymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument) + { + mappedSourcePropertyType = null; + enumerableTypeArgument = null; + + if (!Diagnostics.IsEmpty()) + { + return false; + } + + if (!property.TryGetTypeSymbol(out var propertyType)) + { + AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property)); + return false; + } + + var mapFromAttribute = propertyType.GetAttribute(MapFromAttributeTypeSymbol); + if (mapFromAttribute is null && + propertyType is INamedTypeSymbol namedTypeSymbol && + !propertyType.IsPrimitiveType() && + (Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i)))) + { + enumerableTypeArgument = namedTypeSymbol.TypeArguments.First(); + mapFromAttribute = enumerableTypeArgument.GetAttribute(MapFromAttributeTypeSymbol); + } + + mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol; + + if (mappedSourcePropertyType is null && enumerableTypeArgument is null) + { + AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property)); + } + + return Diagnostics.IsEmpty(); + } + protected bool IsEnumerable(ISymbol property, out INamedTypeSymbol? namedTypeSymbolResult) + { + + if (!property.TryGetTypeSymbol(out var propertyType)) + { + AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property)); + namedTypeSymbolResult = null; + return false; + } + + if ( + propertyType is INamedTypeSymbol namedTypeSymbol && + !propertyType.IsPrimitiveType() && + (Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i)))) + { + namedTypeSymbolResult = namedTypeSymbol; + return true; + } + namedTypeSymbolResult = null; + return false; + } + private static ImmutableArray GetTypeConverterParameters(AttributeData typeConverterAttribute) + { + var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault(); + return converterParameter.IsNull + ? ImmutableArray.Empty + : converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray(); + } + + private MappingModel? CreateMappingModel() + { + var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(TypeSyntax) is not INamedTypeSymbol typeSymbol) + { + AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(TypeSyntax.GetLocation(), TypeSyntax.Identifier.ValueText)); + return null; + } + + var sourceTypeSymbol = GetSourceTypeSymbol(TypeSyntax, semanticModel); + if (sourceTypeSymbol is null) + { + AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation())); + return null; + } + + _ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First()); + + var typeIdentifierName = TypeSyntax.GetIdentifierName(); + var sourceTypeIdentifierName = sourceTypeSymbol.Name; + var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel); + var isTypeUpdatable = IsTypeUpdatable(); + var hasJsonExtension = HasJsonExtension(); + var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol); + + var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); + var mappedFields = GetSourceMappedFields(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); + + /*if (!mappedProperties.Any()) + { + AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol)); + return null; + }*/ + + AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq"); + + var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass); + var allFields = GetTypeMappedFields(sourceTypeSymbol, typeSymbol, isTypeInheritFromMappedBaseClass); + + return new MappingModel( + SourceGenerationOptions, + TypeSyntax.GetNamespace(), + TypeSyntax.Modifiers, + TypeSyntax.Keyword.Text, + typeIdentifierName, + sourceTypeSymbol.ContainingNamespace.ToDisplayString(), + sourceTypeIdentifierName, + sourceTypeSymbol.ToDisplayString(), + isTypeUpdatable, + hasJsonExtension, + mappedProperties, + allProperties, + mappedFields, + allFields, + isTypeInheritFromMappedBaseClass, + Usings, + shouldGenerateSecondaryConstructor); + } + + + + private INamedTypeSymbol? GetTypeConverterBaseInterfaceForProperty(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty) + { + if (!property.TryGetTypeSymbol(out var propertyType)) + { + return null; + } + + return converterTypeSymbol.AllInterfaces + .SingleOrDefault(i => + i.TypeArguments.Length == 2 && + SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) && + SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && + SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1])); + } + private INamedTypeSymbol? GetTypeConverterBaseInterfaceForField(ITypeSymbol converterTypeSymbol, ISymbol property, IFieldSymbol sourceProperty) + { + if (!property.TryGetTypeSymbol(out var propertyType)) + { + return null; + } + + return converterTypeSymbol.AllInterfaces + .SingleOrDefault(i => + i.TypeArguments.Length == 2 && + SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) && + SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && + SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1])); + } + + private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol) + { + var constructorSyntax = TypeSyntax.DescendantNodes() + .OfType() + .SingleOrDefault(c => + c.ParameterList.Parameters.Count == 1 && + SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(c.ParameterList.Parameters.Single().Type!).ConvertedType, sourceTypeSymbol)); + + if (constructorSyntax is null) + { + // Secondary constructor is not defined. + return true; + } + + if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments || + !SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, MappingContextTypeSymbol) || + !SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol)) + { + AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax)); + } + + return false; + } + + private string? ToQualifiedDisplayName(ISymbol? symbol) + { + if (symbol is null) + { + return null; + } + + var containingNamespace = TypeSyntax.GetNamespace(); + var symbolNamespace = symbol.ContainingNamespace.ToDisplayString(); + return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First()) + ? symbol.ToDisplayString() + : symbol.Name; + } + } } \ No newline at end of file diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index e7cb893..e10dcbf 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -1,90 +1,94 @@ -using System; -using System.Collections.Immutable; -using MapTo.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -namespace MapTo -{ - internal enum AccessModifier - { - Public, - Internal, - Private - } - - internal enum NullStaticAnalysisState - { - Default, - Enabled, - Disabled - } - - internal record SourceCode(string Text, string HintName); - - internal record MappedMember( - string Name, - string FullyQualifiedType, - string Type, - string? TypeConverter, - ImmutableArray TypeConverterParameters, - string SourcePropertyName, - string? MappedSourcePropertyTypeName, - string? EnumerableTypeArgument, - bool isReadOnly) - { - public bool IsEnumerable => EnumerableTypeArgument is not null; - } - - internal record MappingModel ( - SourceGenerationOptions Options, - string? Namespace, - SyntaxTokenList Modifiers, - string Type, - string TypeIdentifierName, - string SourceNamespace, - string SourceTypeIdentifierName, - string SourceTypeFullName, - bool IsTypeUpdatable, - ImmutableArray SourceProperties, - ImmutableArray TypeProperties, - ImmutableArray SourceFields, - ImmutableArray TypeFields, - bool HasMappedBaseClass, - ImmutableArray Usings, - bool GenerateSecondaryConstructor - ) - { - public string SourceType => SourceTypeFullName; - } - - internal record SourceGenerationOptions( - AccessModifier ConstructorAccessModifier, - AccessModifier GeneratedMethodsAccessModifier, - bool GenerateXmlDocument, - bool SupportNullableReferenceTypes, - bool SupportNullableStaticAnalysis) - { - internal static SourceGenerationOptions From(GeneratorExecutionContext context) - { - const string allowNullAttributeName = "System.Diagnostics.CodeAnalysis.AllowNullAttribute"; - var supportNullableStaticAnalysis = context.GetBuildGlobalOption(propertyName: nameof(SupportNullableStaticAnalysis), NullStaticAnalysisState.Default); - var supportNullableReferenceTypes = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Warnings or NullableContextOptions.Enable; - - return new( - ConstructorAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(ConstructorAccessModifier), AccessModifier.Public), - GeneratedMethodsAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(GeneratedMethodsAccessModifier), AccessModifier.Public), - GenerateXmlDocument: context.GetBuildGlobalOption(propertyName: nameof(GenerateXmlDocument), true), - SupportNullableReferenceTypes: supportNullableReferenceTypes, - SupportNullableStaticAnalysis: supportNullableStaticAnalysis switch - { - NullStaticAnalysisState.Enabled => true, - NullStaticAnalysisState.Disabled => false, - _ => context.Compilation is CSharpCompilation { LanguageVersion: >= LanguageVersion.CSharp8 } cs && cs.TypeByMetadataNameExists(allowNullAttributeName) - } - ); - } - - public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty; - } +using System; +using System.Collections.Immutable; +using MapTo.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace MapTo +{ + internal enum AccessModifier + { + Public, + Internal, + Private + } + + internal enum NullStaticAnalysisState + { + Default, + Enabled, + Disabled + } + + internal record SourceCode(string Text, string HintName); + + internal record MappedMember( + string Name, + string FullyQualifiedType, + string Type, + string? TypeConverter, + ImmutableArray TypeConverterParameters, + string SourcePropertyName, + string? MappedSourcePropertyTypeName, + string? EnumerableTypeArgument, + ISymbol ActualSymbol, + INamedTypeSymbol? NamedTypeSymbol, + bool isEnumerable, + bool isReadOnly) + { + public bool IsEnumerable => EnumerableTypeArgument is not null; + } + + internal record MappingModel ( + SourceGenerationOptions Options, + string? Namespace, + SyntaxTokenList Modifiers, + string Type, + string TypeIdentifierName, + string SourceNamespace, + string SourceTypeIdentifierName, + string SourceTypeFullName, + bool IsTypeUpdatable, + bool IsJsonExtension, + ImmutableArray SourceProperties, + ImmutableArray TypeProperties, + ImmutableArray SourceFields, + ImmutableArray TypeFields, + bool HasMappedBaseClass, + ImmutableArray Usings, + bool GenerateSecondaryConstructor + ) + { + public string SourceType => SourceTypeFullName; + } + + internal record SourceGenerationOptions( + AccessModifier ConstructorAccessModifier, + AccessModifier GeneratedMethodsAccessModifier, + bool GenerateXmlDocument, + bool SupportNullableReferenceTypes, + bool SupportNullableStaticAnalysis) + { + internal static SourceGenerationOptions From(GeneratorExecutionContext context) + { + const string allowNullAttributeName = "System.Diagnostics.CodeAnalysis.AllowNullAttribute"; + var supportNullableStaticAnalysis = context.GetBuildGlobalOption(propertyName: nameof(SupportNullableStaticAnalysis), NullStaticAnalysisState.Default); + var supportNullableReferenceTypes = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Warnings or NullableContextOptions.Enable; + + return new( + ConstructorAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(ConstructorAccessModifier), AccessModifier.Public), + GeneratedMethodsAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(GeneratedMethodsAccessModifier), AccessModifier.Public), + GenerateXmlDocument: context.GetBuildGlobalOption(propertyName: nameof(GenerateXmlDocument), true), + SupportNullableReferenceTypes: supportNullableReferenceTypes, + SupportNullableStaticAnalysis: supportNullableStaticAnalysis switch + { + NullStaticAnalysisState.Enabled => true, + NullStaticAnalysisState.Disabled => false, + _ => context.Compilation is CSharpCompilation { LanguageVersion: >= LanguageVersion.CSharp8 } cs && cs.TypeByMetadataNameExists(allowNullAttributeName) + } + ); + } + + public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty; + } } \ No newline at end of file diff --git a/src/MapTo/RecordMappingContext.cs b/src/MapTo/RecordMappingContext.cs index 619236c..c56fe2b 100644 --- a/src/MapTo/RecordMappingContext.cs +++ b/src/MapTo/RecordMappingContext.cs @@ -1,52 +1,52 @@ -using System.Collections.Immutable; -using System.Linq; -using MapTo.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo -{ - internal class RecordMappingContext : MappingContext - { - internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) - : base(compilation, sourceGenerationOptions, typeSyntax) { } - - protected override ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - throw new System.NotImplementedException(); - } - - protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - return typeSymbol.GetMembers() - .OfType() - .OrderByDescending(s => s.Parameters.Length) - .First(s => s.Name == ".ctor") - .Parameters - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - throw new System.NotImplementedException(); - } - - protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - return typeSymbol.GetMembers() - .OfType() - .OrderByDescending(s => s.Parameters.Length) - .First(s => s.Name == ".ctor") - .Parameters - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapProperty(typeSymbol, sourceProperties, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - } +using System.Collections.Immutable; +using System.Linq; +using MapTo.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo +{ + internal class RecordMappingContext : MappingContext + { + internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) + : base(compilation, sourceGenerationOptions, typeSyntax) { } + + protected override ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + throw new System.NotImplementedException(); + } + + protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + return typeSymbol.GetMembers() + .OfType() + .OrderByDescending(s => s.Parameters.Length) + .First(s => s.Name == ".ctor") + .Parameters + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + throw new System.NotImplementedException(); + } + + protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + return typeSymbol.GetMembers() + .OfType() + .OrderByDescending(s => s.Parameters.Length) + .First(s => s.Name == ".ctor") + .Parameters + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapProperty(typeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/Constants.cs b/src/MapTo/Sources/Constants.cs index f25d2ce..66e80c2 100644 --- a/src/MapTo/Sources/Constants.cs +++ b/src/MapTo/Sources/Constants.cs @@ -1,8 +1,8 @@ -namespace MapTo.Sources -{ - internal class Constants - { - internal const string RootNamespace = "MapTo"; - internal const string GeneratedFilesHeader = "// "; - } +namespace MapTo.Sources +{ + internal class Constants + { + internal const string RootNamespace = "MapTo"; + internal const string GeneratedFilesHeader = "// "; + } } \ No newline at end of file diff --git a/src/MapTo/Sources/DictionaryToListAttributeSource.cs b/src/MapTo/Sources/DictionaryToListAttributeSource.cs new file mode 100644 index 0000000..4429fd8 --- /dev/null +++ b/src/MapTo/Sources/DictionaryToListAttributeSource.cs @@ -0,0 +1,60 @@ +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class DictionaryToListAttributeSource + { + internal const string AttributeName = "DictionaryToList"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + internal const string SourceMemberNameFieldOrPropertyName = "SourcePropertyName"; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) + .WriteLine() + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies the mapping behavior of the annotated property.") + .WriteLine("/// ") + .WriteLine("/// ") + .WriteLine($"/// {AttributeClassName} has a number of uses:") + .WriteLine("/// ") + .WriteLine("/// By default properties with same name will get mapped. This attribute allows the names to be different.") + .WriteLine("/// Indicates that a property should be mapped when member serialization is set to opt-in.") + .WriteLine("/// ") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]") + .WriteLine($"public sealed class {AttributeClassName} : Attribute") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Gets or sets the property name of the object to mapping from.") + .WriteLine("/// "); + } + + builder + .WriteLine($"public string{options.NullableReferenceSyntax} {SourceMemberNameFieldOrPropertyName} {{ get; set; }}") + .WriteClosingBracket() // class + .WriteClosingBracket(); // namespace + + + return new(builder.ToString(), $"{AttributeClassName}.g.cs"); + } + } +} \ No newline at end of file diff --git a/src/MapTo/Sources/ITypeConverterSource.cs b/src/MapTo/Sources/ITypeConverterSource.cs index 3b627b8..3bb41d5 100644 --- a/src/MapTo/Sources/ITypeConverterSource.cs +++ b/src/MapTo/Sources/ITypeConverterSource.cs @@ -1,58 +1,58 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - [SuppressMessage("ReSharper", "InconsistentNaming")] - internal static class ITypeConverterSource - { - internal const string InterfaceName = "ITypeConverter"; - internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2"; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) - .WriteLine() - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Converts the value of to .") - .WriteLine("/// ") - .WriteLine("/// The type to convert from.") - .WriteLine("/// The type to convert to."); - } - - builder - .WriteLine($"public interface {InterfaceName}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Converts the value of object to .") - .WriteLine("/// ") - .WriteLine("/// The to convert.") - .WriteLine($"/// The parameter list passed to the ") - .WriteLine("/// object."); - } - - builder - .WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);") - .WriteClosingBracket() - .WriteClosingBracket(); - - return new(builder.ToString(), $"{InterfaceName}.g.cs"); - } - - internal static string GetFullyQualifiedName(ITypeSymbol sourceType, ITypeSymbol destinationType) => - $"{RootNamespace}.{InterfaceName}<{sourceType.ToDisplayString()}, {destinationType.ToDisplayString()}>"; - } +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class ITypeConverterSource + { + internal const string InterfaceName = "ITypeConverter"; + internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2"; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Converts the value of to .") + .WriteLine("/// ") + .WriteLine("/// The type to convert from.") + .WriteLine("/// The type to convert to."); + } + + builder + .WriteLine($"public interface {InterfaceName}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Converts the value of object to .") + .WriteLine("/// ") + .WriteLine("/// The to convert.") + .WriteLine($"/// The parameter list passed to the ") + .WriteLine("/// object."); + } + + builder + .WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);") + .WriteClosingBracket() + .WriteClosingBracket(); + + return new(builder.ToString(), $"{InterfaceName}.g.cs"); + } + + internal static string GetFullyQualifiedName(ITypeSymbol sourceType, ITypeSymbol destinationType) => + $"{RootNamespace}.{InterfaceName}<{sourceType.ToDisplayString()}, {destinationType.ToDisplayString()}>"; + } } \ No newline at end of file diff --git a/src/MapTo/Sources/IgnorePropertyAttributeSource.cs b/src/MapTo/Sources/IgnoreMemberAttributeSource.cs similarity index 83% rename from src/MapTo/Sources/IgnorePropertyAttributeSource.cs rename to src/MapTo/Sources/IgnoreMemberAttributeSource.cs index a770d22..502dfa4 100644 --- a/src/MapTo/Sources/IgnorePropertyAttributeSource.cs +++ b/src/MapTo/Sources/IgnoreMemberAttributeSource.cs @@ -1,36 +1,36 @@ -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class IgnorePropertyAttributeSource - { - internal const string AttributeName = "IgnoreProperty"; - internal const string AttributeClassName = AttributeName + "Attribute"; - internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteLine("using System;") - .WriteLine() - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Specifies that the annotated property should be excluded.") - .WriteLine("/// "); - } - - builder - .WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]") - .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}") - .WriteClosingBracket(); - - return new(builder.ToString(), $"{AttributeClassName}.g.cs"); - } - } +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class IgnoreMemberAttributeSource + { + internal const string AttributeName = "IgnoreMemberMapTo"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies that the annotated property should be excluded.") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = false)]") + .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}") + .WriteClosingBracket(); + + return new(builder.ToString(), $"{AttributeClassName}.g.cs"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/JsonExtensionAttributeSource.cs b/src/MapTo/Sources/JsonExtensionAttributeSource.cs new file mode 100644 index 0000000..d556fe5 --- /dev/null +++ b/src/MapTo/Sources/JsonExtensionAttributeSource.cs @@ -0,0 +1,40 @@ +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class JsonExtensionAttributeSource + { + internal const string AttributeName = "JsonExtension"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies that the annotated class has a json extension.") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") + .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") + .WriteOpeningBracket(); + + builder + .WriteClosingBracket() // class + .WriteClosingBracket(); // namespace + + return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); + } + } +} \ No newline at end of file diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 9278fe6..437926b 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -1,14 +1,14 @@ -using MapTo.Extensions; -using System.Text; -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class MapClassSource - { - internal static SourceCode Generate(MappingModel model) - { - return model.GenerateStructOrClass("class"); - } - } +using MapTo.Extensions; +using System.Text; +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class MapClassSource + { + internal static SourceCode Generate(MappingModel model) + { + return model.GenerateStructOrClass("class"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapFromAttributeSource.cs b/src/MapTo/Sources/MapFromAttributeSource.cs index 6594832..70c48ef 100644 --- a/src/MapTo/Sources/MapFromAttributeSource.cs +++ b/src/MapTo/Sources/MapFromAttributeSource.cs @@ -1,65 +1,65 @@ -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class MapFromAttributeSource - { - internal const string AttributeName = "MapFrom"; - internal const string AttributeClassName = AttributeName + "Attribute"; - internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteLine("using System;") - .WriteLine() - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Specifies that the annotated class can be mapped from the provided .") - .WriteLine("/// "); - } - - builder - .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") - .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Initializes a new instance of the class with the specified .") - .WriteLine("/// ") - .WriteLine("/// The type of to map from."); - } - - builder - .WriteLine($"public {AttributeName}Attribute(Type sourceType)") - .WriteOpeningBracket() - .WriteLine("SourceType = sourceType;") - .WriteClosingBracket() - .WriteLine(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Gets the type to map from.") - .WriteLine("/// "); - } - - builder - .WriteLine("public Type SourceType { get; }") - .WriteClosingBracket() // class - .WriteClosingBracket(); // namespace - - return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); - } - } +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class MapFromAttributeSource + { + internal const string AttributeName = "MapFrom"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies that the annotated class can be mapped from the provided .") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") + .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine($"/// Initializes a new instance of the class with the specified .") + .WriteLine("/// ") + .WriteLine("/// The type of to map from."); + } + + builder + .WriteLine($"public {AttributeName}Attribute(Type sourceType)") + .WriteOpeningBracket() + .WriteLine("SourceType = sourceType;") + .WriteClosingBracket() + .WriteLine(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Gets the type to map from.") + .WriteLine("/// "); + } + + builder + .WriteLine("public Type SourceType { get; }") + .WriteClosingBracket() // class + .WriteClosingBracket(); // namespace + + return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapPropertyAttributeSource.cs b/src/MapTo/Sources/MapPropertyAttributeSource.cs index 7574204..4c18cae 100644 --- a/src/MapTo/Sources/MapPropertyAttributeSource.cs +++ b/src/MapTo/Sources/MapPropertyAttributeSource.cs @@ -1,60 +1,60 @@ -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class MapPropertyAttributeSource - { - internal const string AttributeName = "MapProperty"; - internal const string AttributeClassName = AttributeName + "Attribute"; - internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; - internal const string SourcePropertyNamePropertyName = "SourcePropertyName"; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) - .WriteLine() - .WriteLine("using System;") - .WriteLine() - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Specifies the mapping behavior of the annotated property.") - .WriteLine("/// ") - .WriteLine("/// ") - .WriteLine($"/// {AttributeClassName} has a number of uses:") - .WriteLine("/// ") - .WriteLine("/// By default properties with same name will get mapped. This attribute allows the names to be different.") - .WriteLine("/// Indicates that a property should be mapped when member serialization is set to opt-in.") - .WriteLine("/// ") - .WriteLine("/// "); - } - - builder - .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]") - .WriteLine($"public sealed class {AttributeClassName} : Attribute") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Gets or sets the property name of the object to mapping from.") - .WriteLine("/// "); - } - - builder - .WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}") - .WriteClosingBracket() // class - .WriteClosingBracket(); // namespace - - - return new(builder.ToString(), $"{AttributeClassName}.g.cs"); - } - } +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class MapPropertyAttributeSource + { + internal const string AttributeName = "MapProperty"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + internal const string SourcePropertyNamePropertyName = "SourcePropertyName"; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) + .WriteLine() + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies the mapping behavior of the annotated property.") + .WriteLine("/// ") + .WriteLine("/// ") + .WriteLine($"/// {AttributeClassName} has a number of uses:") + .WriteLine("/// ") + .WriteLine("/// By default properties with same name will get mapped. This attribute allows the names to be different.") + .WriteLine("/// Indicates that a property should be mapped when member serialization is set to opt-in.") + .WriteLine("/// ") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]") + .WriteLine($"public sealed class {AttributeClassName} : Attribute") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Gets or sets the property name of the object to mapping from.") + .WriteLine("/// "); + } + + builder + .WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}") + .WriteClosingBracket() // class + .WriteClosingBracket(); // namespace + + + return new(builder.ToString(), $"{AttributeClassName}.g.cs"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapRecordSource.cs b/src/MapTo/Sources/MapRecordSource.cs index 8107acb..baa48b5 100644 --- a/src/MapTo/Sources/MapRecordSource.cs +++ b/src/MapTo/Sources/MapRecordSource.cs @@ -1,191 +1,191 @@ -using System; -using MapTo.Extensions; -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class MapRecordSource - { - internal static SourceCode Generate(MappingModel model) - { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) - .WriteUsings(model.Usings) - .WriteLine() - - // Namespace declaration - .WriteLine($"namespace {model.Namespace}") - .WriteOpeningBracket() - - // Class declaration - .WriteLine($"partial record {model.TypeIdentifierName}") - .WriteOpeningBracket(); - - // Class body - if (model.GenerateSecondaryConstructor) - { - builder - .GenerateSecondaryConstructor(model) - .WriteLine(); - } - - builder - .GeneratePrivateConstructor(model) - - .WriteLine() - .GenerateFactoryMethod(model) - - // End class declaration - .WriteClosingBracket() - .WriteLine() - - // Extension class declaration - .GenerateSourceTypeExtensionClass(model) - - - // End namespace declaration - .WriteClosingBracket(); - - return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); - } - - private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - if (model.Options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Initializes a new instance of the class") - .WriteLine($"/// using the property values from the specified .") - .WriteLine("/// ") - .WriteLine($"/// {sourceClassParameterName} is null"); - } - - return builder - .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})") - .WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}"); - } - - private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - const string mappingContextParameterName = "context"; - - builder - .WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName})") - .Indent() - .Write(": this("). - - WriteProperties(model, sourceClassParameterName, mappingContextParameterName) - - .WriteLine(")") - .Unindent() - .WriteOpeningBracket() - .WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));") - .WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));") - .WriteLine() - .WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);"); - - // End constructor declaration - return builder.WriteClosingBracket(); - } - - private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName, - string mappingContextParameterName) - { - for (var i = 0; i < model.SourceProperties.Length; i++) - { - var property = model.SourceProperties[i]; - if (property.TypeConverter is null) - { - if (property.IsEnumerable) - { - builder.Write( - $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList()"); - } - else - { - builder.Write(property.MappedSourcePropertyTypeName is null - ? $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}" - : $"{property.Name}: {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName})"); - } - } - else - { - var parameters = property.TypeConverterParameters.IsEmpty - ? "null" - : $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}"; - - builder.Write( - $"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})"); - } - - if (i < model.SourceProperties.Length - 1) - { - builder.Write(", "); - } - } - - return builder; - } - - private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - return builder - .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) - .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") - .WriteLine( - $"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") - .WriteOpeningBracket() - .WriteLine( - $"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});") - .WriteClosingBracket(); - } - - private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) - { - if (!model.Options.GenerateXmlDocument) - { - return builder; - } - - return builder - .WriteLine("/// ") - .WriteLine($"/// Creates a new instance of and sets its participating properties") - .WriteLine($"/// using the property values from .") - .WriteLine("/// ") - .WriteLine($"/// The instance of to use as source.") - .WriteLine( - $"/// A new instance of -or- null if is null."); - } - - private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model) - { - return builder - .WriteLine( - $"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions") - .WriteOpeningBracket() - .GenerateSourceTypeExtensionMethod(model) - .WriteClosingBracket(); - } - - private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - return builder - .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) - .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") - .WriteLine( - $"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") - .WriteOpeningBracket() - .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});") - .WriteClosingBracket(); - } - } +using System; +using MapTo.Extensions; +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class MapRecordSource + { + internal static SourceCode Generate(MappingModel model) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) + .WriteUsings(model.Usings) + .WriteLine() + + // Namespace declaration + .WriteLine($"namespace {model.Namespace}") + .WriteOpeningBracket() + + // Class declaration + .WriteLine($"partial record {model.TypeIdentifierName}") + .WriteOpeningBracket(); + + // Class body + if (model.GenerateSecondaryConstructor) + { + builder + .GenerateSecondaryConstructor(model) + .WriteLine(); + } + + builder + .GeneratePrivateConstructor(model) + + .WriteLine() + .GenerateFactoryMethod(model) + + // End class declaration + .WriteClosingBracket() + .WriteLine() + + // Extension class declaration + .GenerateSourceTypeExtensionClass(model) + + + // End namespace declaration + .WriteClosingBracket(); + + return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); + } + + private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + + if (model.Options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine($"/// Initializes a new instance of the class") + .WriteLine($"/// using the property values from the specified .") + .WriteLine("/// ") + .WriteLine($"/// {sourceClassParameterName} is null"); + } + + return builder + .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})") + .WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}"); + } + + private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + const string mappingContextParameterName = "context"; + + builder + .WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName})") + .Indent() + .Write(": this("). + + WriteProperties(model, sourceClassParameterName, mappingContextParameterName) + + .WriteLine(")") + .Unindent() + .WriteOpeningBracket() + .WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));") + .WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));") + .WriteLine() + .WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);"); + + // End constructor declaration + return builder.WriteClosingBracket(); + } + + private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName, + string mappingContextParameterName) + { + for (var i = 0; i < model.SourceProperties.Length; i++) + { + var property = model.SourceProperties[i]; + if (property.TypeConverter is null) + { + if (property.IsEnumerable) + { + builder.Write( + $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList()"); + } + else + { + builder.Write(property.MappedSourcePropertyTypeName is null + ? $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}" + : $"{property.Name}: {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName})"); + } + } + else + { + var parameters = property.TypeConverterParameters.IsEmpty + ? "null" + : $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}"; + + builder.Write( + $"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})"); + } + + if (i < model.SourceProperties.Length - 1) + { + builder.Write(", "); + } + } + + return builder; + } + + private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + + return builder + .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) + .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") + .WriteLine( + $"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") + .WriteOpeningBracket() + .WriteLine( + $"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});") + .WriteClosingBracket(); + } + + private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) + { + if (!model.Options.GenerateXmlDocument) + { + return builder; + } + + return builder + .WriteLine("/// ") + .WriteLine($"/// Creates a new instance of and sets its participating properties") + .WriteLine($"/// using the property values from .") + .WriteLine("/// ") + .WriteLine($"/// The instance of to use as source.") + .WriteLine( + $"/// A new instance of -or- null if is null."); + } + + private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model) + { + return builder + .WriteLine( + $"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions") + .WriteOpeningBracket() + .GenerateSourceTypeExtensionMethod(model) + .WriteClosingBracket(); + } + + private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + + return builder + .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) + .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") + .WriteLine( + $"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") + .WriteOpeningBracket() + .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});") + .WriteClosingBracket(); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapStructSource.cs b/src/MapTo/Sources/MapStructSource.cs index 32f4960..5a8b60e 100644 --- a/src/MapTo/Sources/MapStructSource.cs +++ b/src/MapTo/Sources/MapStructSource.cs @@ -1,15 +1,15 @@ -using MapTo.Extensions; -using static MapTo.Sources.Constants; -using System.Collections.Generic; -using System.Text; - -namespace MapTo.Sources -{ - internal static class MapStructSource - { - internal static SourceCode Generate(MappingModel model) - { - return model.GenerateStructOrClass("struct"); - } - } +using MapTo.Extensions; +using static MapTo.Sources.Constants; +using System.Collections.Generic; +using System.Text; + +namespace MapTo.Sources +{ + internal static class MapStructSource + { + internal static SourceCode Generate(MappingModel model) + { + return model.GenerateStructOrClass("struct"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapTypeConverterAttributeSource.cs b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs index f9efa26..6d2a6a3 100644 --- a/src/MapTo/Sources/MapTypeConverterAttributeSource.cs +++ b/src/MapTo/Sources/MapTypeConverterAttributeSource.cs @@ -1,84 +1,84 @@ -using System; -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class MapTypeConverterAttributeSource - { - internal const string AttributeName = "MapTypeConverter"; - internal const string AttributeClassName = AttributeName + "Attribute"; - internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; - internal const string ConverterPropertyName = "Converter"; - internal const string ConverterParametersPropertyName = "ConverterParameters"; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) - .WriteLine() - .WriteLine("using System;") - .WriteLine() - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Specifies what type to use as a converter for the property this attribute is bound to.") - .WriteLine("/// "); - } - - builder - .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]") - .WriteLine($"public sealed class {AttributeClassName} : Attribute") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Initializes a new instance of .") - .WriteLine("/// ") - .WriteLine($"/// The to be used to convert the source type.") - .WriteLine("/// The list of parameters to pass to the during the type conversion."); - } - - builder - .WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)") - .WriteOpeningBracket() - .WriteLine($"{ConverterPropertyName} = converter;") - .WriteLine($"{ConverterParametersPropertyName} = converterParameters;") - .WriteClosingBracket() - .WriteLine(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Gets or sets the to be used to convert the source type.") - .WriteLine("/// "); - } - - builder - .WriteLine($"public Type {ConverterPropertyName} {{ get; }}") - .WriteLine(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Gets the list of parameters to pass to the during the type conversion.") - .WriteLine("/// "); - } - - builder - .WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}") - .WriteClosingBracket() - .WriteClosingBracket(); - - return new(builder.ToString(), $"{AttributeClassName}.g.cs"); - } - } +using System; +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class MapTypeConverterAttributeSource + { + internal const string AttributeName = "MapTypeConverter"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + internal const string ConverterPropertyName = "Converter"; + internal const string ConverterParametersPropertyName = "ConverterParameters"; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) + .WriteLine() + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies what type to use as a converter for the property this attribute is bound to.") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]") + .WriteLine($"public sealed class {AttributeClassName} : Attribute") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine($"/// Initializes a new instance of .") + .WriteLine("/// ") + .WriteLine($"/// The to be used to convert the source type.") + .WriteLine("/// The list of parameters to pass to the during the type conversion."); + } + + builder + .WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)") + .WriteOpeningBracket() + .WriteLine($"{ConverterPropertyName} = converter;") + .WriteLine($"{ConverterParametersPropertyName} = converterParameters;") + .WriteClosingBracket() + .WriteLine(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine($"/// Gets or sets the to be used to convert the source type.") + .WriteLine("/// "); + } + + builder + .WriteLine($"public Type {ConverterPropertyName} {{ get; }}") + .WriteLine(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine($"/// Gets the list of parameters to pass to the during the type conversion.") + .WriteLine("/// "); + } + + builder + .WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}") + .WriteClosingBracket() + .WriteClosingBracket(); + + return new(builder.ToString(), $"{AttributeClassName}.g.cs"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/MappingContextSource.cs b/src/MapTo/Sources/MappingContextSource.cs index 76b6286..43dbc73 100644 --- a/src/MapTo/Sources/MappingContextSource.cs +++ b/src/MapTo/Sources/MappingContextSource.cs @@ -1,115 +1,115 @@ -using System.Collections.Generic; -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class MappingContextSource - { - internal const string ClassName = "MappingContext"; - internal const string FullyQualifiedName = RootNamespace + "." + ClassName; - internal const string FactoryMethodName = "Create"; - internal const string RegisterMethodName = "Register"; - internal const string MapMethodName = "MapFromWithContext"; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - var usings = new List { "System", "System.Collections.Generic", "System.Reflection" }; - - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteLine() - .WriteUsings(usings) - .WriteLine() - - // Namespace declaration - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket() - - // Class declaration - .WriteLine($"internal sealed class {ClassName}") - .WriteOpeningBracket() - - .WriteLine("private readonly Dictionary _cache;") - .WriteLine() - - // Constructor - .WriteLine($"internal {ClassName}()") - .WriteOpeningBracket() - .WriteLine("_cache = new Dictionary(1);") - .WriteClosingBracket() - .WriteLine() - - // Factory - .WriteLine($"internal static TMapped {FactoryMethodName}(TOriginal original)") - .WriteOpeningBracket() - .WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));") - .WriteLine() - .WriteLine("var context = new MappingContext();") - .WriteLine("var mapped = context.MapFromWithContext(original);") - .WriteLine() - .WriteLine("if (mapped == null)") - .WriteOpeningBracket() - .WriteLine("throw new InvalidOperationException();") - .WriteClosingBracket() - .WriteLine() - .WriteLine("return mapped;") - .WriteClosingBracket() - .WriteLine() - - // MapFromWithContext method - .WriteLine($"internal TMapped MapFromWithContext(TOriginal original)") - .WriteOpeningBracket() - .WriteLine("if (original == null)") - .WriteOpeningBracket() - .WriteLine("return default(TMapped);") - .WriteClosingBracket() - .WriteLine() - .WriteLine("if (!TryGetValue(original, out var mapped))") - .WriteOpeningBracket() - .WriteLine("var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);") - .WriteLine("if (instance != null)") - .WriteOpeningBracket() - .WriteLine("mapped = (TMapped)instance;") - .WriteClosingBracket() - .WriteClosingBracket() - .WriteLine() - .WriteLine("return mapped;") - .WriteClosingBracket() - .WriteLine() - - // Register method - .WriteLine("internal void Register(TOriginal original, TMapped mapped)") - .WriteOpeningBracket() - .WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));") - .WriteLine("if (mapped == null) throw new ArgumentNullException(nameof(mapped));") - .WriteLine() - .WriteLine("if (!_cache.ContainsKey(original))") - .WriteOpeningBracket() - .WriteLine("_cache.Add(original, mapped);") - .WriteClosingBracket() - .WriteClosingBracket() - .WriteLine() - - // TryGetValue method - .WriteLine("private bool TryGetValue(TOriginal original, out TMapped mapped)") - .WriteOpeningBracket() - .WriteLine("if (original != null && _cache.TryGetValue(original, out var value))") - .WriteOpeningBracket() - .WriteLine("mapped = (TMapped)value;") - .WriteLine("return true;") - .WriteClosingBracket() - .WriteLine() - .WriteLine("mapped = default(TMapped);") - .WriteLine("return false;") - .WriteClosingBracket() - - // End class declaration - .WriteClosingBracket() - - // End namespace declaration - .WriteClosingBracket(); - - return new(builder.ToString(), $"{ClassName}.g.cs"); - } - } +using System.Collections.Generic; +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class MappingContextSource + { + internal const string ClassName = "MappingContext"; + internal const string FullyQualifiedName = RootNamespace + "." + ClassName; + internal const string FactoryMethodName = "Create"; + internal const string RegisterMethodName = "Register"; + internal const string MapMethodName = "MapFromWithContext"; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + var usings = new List { "System", "System.Collections.Generic", "System.Reflection" }; + + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine() + .WriteUsings(usings) + .WriteLine() + + // Namespace declaration + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket() + + // Class declaration + .WriteLine($"internal sealed class {ClassName}") + .WriteOpeningBracket() + + .WriteLine("private readonly Dictionary _cache;") + .WriteLine() + + // Constructor + .WriteLine($"internal {ClassName}()") + .WriteOpeningBracket() + .WriteLine("_cache = new Dictionary(1);") + .WriteClosingBracket() + .WriteLine() + + // Factory + .WriteLine($"internal static TMapped {FactoryMethodName}(TOriginal original)") + .WriteOpeningBracket() + .WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));") + .WriteLine() + .WriteLine("var context = new MappingContext();") + .WriteLine("var mapped = context.MapFromWithContext(original);") + .WriteLine() + .WriteLine("if (mapped == null)") + .WriteOpeningBracket() + .WriteLine("throw new InvalidOperationException();") + .WriteClosingBracket() + .WriteLine() + .WriteLine("return mapped;") + .WriteClosingBracket() + .WriteLine() + + // MapFromWithContext method + .WriteLine($"internal TMapped MapFromWithContext(TOriginal original)") + .WriteOpeningBracket() + .WriteLine("if (original == null)") + .WriteOpeningBracket() + .WriteLine("return default(TMapped);") + .WriteClosingBracket() + .WriteLine() + .WriteLine("if (!TryGetValue(original, out var mapped))") + .WriteOpeningBracket() + .WriteLine("var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);") + .WriteLine("if (instance != null)") + .WriteOpeningBracket() + .WriteLine("mapped = (TMapped)instance;") + .WriteClosingBracket() + .WriteClosingBracket() + .WriteLine() + .WriteLine("return mapped;") + .WriteClosingBracket() + .WriteLine() + + // Register method + .WriteLine("internal void Register(TOriginal original, TMapped mapped)") + .WriteOpeningBracket() + .WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));") + .WriteLine("if (mapped == null) throw new ArgumentNullException(nameof(mapped));") + .WriteLine() + .WriteLine("if (!_cache.ContainsKey(original))") + .WriteOpeningBracket() + .WriteLine("_cache.Add(original, mapped);") + .WriteClosingBracket() + .WriteClosingBracket() + .WriteLine() + + // TryGetValue method + .WriteLine("private bool TryGetValue(TOriginal original, out TMapped mapped)") + .WriteOpeningBracket() + .WriteLine("if (original != null && _cache.TryGetValue(original, out var value))") + .WriteOpeningBracket() + .WriteLine("mapped = (TMapped)value;") + .WriteLine("return true;") + .WriteClosingBracket() + .WriteLine() + .WriteLine("mapped = default(TMapped);") + .WriteLine("return false;") + .WriteClosingBracket() + + // End class declaration + .WriteClosingBracket() + + // End namespace declaration + .WriteClosingBracket(); + + return new(builder.ToString(), $"{ClassName}.g.cs"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs b/src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs index 3f0b2c5..babb02f 100644 --- a/src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs +++ b/src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs @@ -1,36 +1,36 @@ -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class ReadOnlyPropertyAttributeSource - { - internal const string AttributeName = "ReadOnlyProperty"; - internal const string AttributeClassName = AttributeName + "Attribute"; - internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteLine("using System;") - .WriteLine() - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Specifies that the annotated property should be excluded.") - .WriteLine("/// "); - } - - builder - .WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]") - .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}") - .WriteClosingBracket(); - - return new(builder.ToString(), $"{AttributeClassName}.g.cs"); - } - } +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class ReadOnlyPropertyAttributeSource + { + internal const string AttributeName = "ReadOnlyProperty"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies that the annotated property should be excluded.") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]") + .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}") + .WriteClosingBracket(); + + return new(builder.ToString(), $"{AttributeClassName}.g.cs"); + } + } } \ No newline at end of file diff --git a/src/MapTo/Sources/SourceBuilder.cs b/src/MapTo/Sources/SourceBuilder.cs index 369fe9e..cb270c6 100644 --- a/src/MapTo/Sources/SourceBuilder.cs +++ b/src/MapTo/Sources/SourceBuilder.cs @@ -1,107 +1,107 @@ -using System; -using System.CodeDom.Compiler; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace MapTo.Sources -{ - internal sealed class SourceBuilder : IDisposable - { - private readonly StringWriter _writer; - private readonly IndentedTextWriter _indentedWriter; - - public SourceBuilder() - { - _writer = new StringWriter(); - _indentedWriter = new IndentedTextWriter(_writer, new string(' ', 4)); - } - - /// - public void Dispose() - { - _writer.Dispose(); - _indentedWriter.Dispose(); - } - - public SourceBuilder WriteLine(string? value = null) - { - if (string.IsNullOrWhiteSpace(value)) - { - _indentedWriter.WriteLineNoTabs(string.Empty); - } - else - { - _indentedWriter.WriteLine(value); - } - - return this; - } - - public SourceBuilder Write(string? value = null) - { - _indentedWriter.Write(value); - return this; - } - - public SourceBuilder WriteLineIf(bool condition, string? value) - { - if (condition) - { - WriteLine(value); - } - - return this; - } - - public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable"); - - public SourceBuilder WriteOpeningBracket() - { - _indentedWriter.WriteLine("{"); - _indentedWriter.Indent++; - - return this; - } - - public SourceBuilder WriteClosingBracket() - { - _indentedWriter.Indent--; - _indentedWriter.WriteLine("}"); - - return this; - } - - public SourceBuilder WriteUsings(IEnumerable usings) - { - foreach (var u in usings.OrderBy(s => s)) - { - WriteUsing(u); - } - - return this; - } - - public SourceBuilder WriteUsing(string u) - { - WriteLine($"using {u};"); - - return this; - } - - public SourceBuilder Indent() - { - _indentedWriter.Indent++; - return this; - } - - public SourceBuilder Unindent() - { - _indentedWriter.Indent--; - return this; - } - - /// - public override string ToString() => _writer.ToString(); - } +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace MapTo.Sources +{ + internal sealed class SourceBuilder : IDisposable + { + private readonly StringWriter _writer; + private readonly IndentedTextWriter _indentedWriter; + + public SourceBuilder() + { + _writer = new StringWriter(); + _indentedWriter = new IndentedTextWriter(_writer, new string(' ', 4)); + } + + /// + public void Dispose() + { + _writer.Dispose(); + _indentedWriter.Dispose(); + } + + public SourceBuilder WriteLine(string? value = null) + { + if (string.IsNullOrWhiteSpace(value)) + { + _indentedWriter.WriteLineNoTabs(string.Empty); + } + else + { + _indentedWriter.WriteLine(value); + } + + return this; + } + + public SourceBuilder Write(string? value = null) + { + _indentedWriter.Write(value); + return this; + } + + public SourceBuilder WriteLineIf(bool condition, string? value) + { + if (condition) + { + WriteLine(value); + } + + return this; + } + + public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable"); + + public SourceBuilder WriteOpeningBracket() + { + _indentedWriter.WriteLine("{"); + _indentedWriter.Indent++; + + return this; + } + + public SourceBuilder WriteClosingBracket() + { + _indentedWriter.Indent--; + _indentedWriter.WriteLine("}"); + + return this; + } + + public SourceBuilder WriteUsings(IEnumerable usings) + { + foreach (var u in usings.OrderBy(s => s)) + { + WriteUsing(u); + } + + return this; + } + + public SourceBuilder WriteUsing(string u) + { + WriteLine($"using {u};"); + + return this; + } + + public SourceBuilder Indent() + { + _indentedWriter.Indent++; + return this; + } + + public SourceBuilder Unindent() + { + _indentedWriter.Indent--; + return this; + } + + /// + public override string ToString() => _writer.ToString(); + } } \ No newline at end of file diff --git a/src/MapTo/Sources/UseUpdateAttributeSource.cs b/src/MapTo/Sources/UseUpdateAttributeSource.cs index dae6d71..befca4a 100644 --- a/src/MapTo/Sources/UseUpdateAttributeSource.cs +++ b/src/MapTo/Sources/UseUpdateAttributeSource.cs @@ -1,40 +1,40 @@ -using static MapTo.Sources.Constants; - -namespace MapTo.Sources -{ - internal static class UseUpdateAttributeSource - { - internal const string AttributeName = "UseUpdate"; - internal const string AttributeClassName = AttributeName + "Attribute"; - internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; - - internal static SourceCode Generate(SourceGenerationOptions options) - { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteLine("using System;") - .WriteLine() - .WriteLine($"namespace {RootNamespace}") - .WriteOpeningBracket(); - - if (options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine("/// Specifies that the annotated class can be updatable.") - .WriteLine("/// "); - } - - builder - .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") - .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") - .WriteOpeningBracket(); - - builder - .WriteClosingBracket() // class - .WriteClosingBracket(); // namespace - - return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); - } - } +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class UseUpdateAttributeSource + { + internal const string AttributeName = "UseUpdate"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies that the annotated class can be updatable.") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") + .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") + .WriteOpeningBracket(); + + builder + .WriteClosingBracket() // class + .WriteClosingBracket(); // namespace + + return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); + } + } } \ No newline at end of file diff --git a/src/MapTo/StructMappingContext.cs b/src/MapTo/StructMappingContext.cs index 52751d1..707bdc9 100644 --- a/src/MapTo/StructMappingContext.cs +++ b/src/MapTo/StructMappingContext.cs @@ -1,67 +1,67 @@ -using System.Collections.Immutable; -using System.Linq; -using MapTo.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace MapTo -{ - internal class StructMappingContext : MappingContext - { - internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) - : base(compilation, sourceGenerationOptions, typeSyntax) { } - - protected override ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return typeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapField(sourceTypeSymbol, sourceProperties, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return typeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return sourceTypeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapFieldSimple(typeSymbol, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) - { - var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); - - return sourceTypeSymbol - .GetAllMembers() - .OfType() - .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) - .Select(property => MapPropertySimple(typeSymbol, property)) - .Where(mappedProperty => mappedProperty is not null) - .ToImmutableArray()!; - } - - } +using System.Collections.Immutable; +using System.Linq; +using MapTo.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo +{ + internal class StructMappingContext : MappingContext + { + internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) + : base(compilation, sourceGenerationOptions, typeSyntax) { } + + protected override ImmutableArray GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return typeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapField(sourceTypeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return typeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + protected override ImmutableArray GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return sourceTypeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapFieldSimple(typeSymbol, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return sourceTypeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol)) + .Select(property => MapPropertySimple(typeSymbol, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + + } } \ No newline at end of file diff --git a/test/MapTo.Integration.Tests/CyclicReferenceTests.cs b/test/MapTo.Integration.Tests/CyclicReferenceTests.cs index 189d16f..071b42c 100644 --- a/test/MapTo.Integration.Tests/CyclicReferenceTests.cs +++ b/test/MapTo.Integration.Tests/CyclicReferenceTests.cs @@ -1,85 +1,85 @@ -using System.Linq; -using MapTo.Integration.Tests.Data.Models; -using MapTo.Integration.Tests.Data.ViewModels; -using Shouldly; -using Xunit; - -namespace MapTo.Integration.Tests -{ - public class CyclicReferenceTests - { - [Fact] - public void VerifySelfReference() - { - // Arrange - var manager = new Manager { Id = 1, EmployeeCode = "M001", Level = 100 }; - manager.Manager = manager; - - // Act - var result = manager.ToManagerViewModel(); - - // Assert - result.Id.ShouldBe(manager.Id); - result.EmployeeCode.ShouldBe(manager.EmployeeCode); - result.Level.ShouldBe(manager.Level); - result.Manager.ShouldBeSameAs(result); - } - - [Fact] - public void VerifyNestedReference() - { - // Arrange - var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 }; - var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 }; - - var employee1 = new Employee { Id = 200, EmployeeCode = "E001"}; - var employee2 = new Employee { Id = 201, EmployeeCode = "E002"}; - - employee1.Manager = manager1; - employee2.Manager = manager2; - - manager2.Manager = manager1; - - // Act - var manager1ViewModel = manager1.ToManagerViewModel(); - - // Assert - manager1ViewModel.Id.ShouldBe(manager1.Id); - manager1ViewModel.Manager.ShouldBeNull(); - manager1ViewModel.Employees.Count.ShouldBe(2); - manager1ViewModel.Employees[0].Id.ShouldBe(employee1.Id); - manager1ViewModel.Employees[0].Manager.ShouldBeSameAs(manager1ViewModel); - manager1ViewModel.Employees[1].Id.ShouldBe(manager2.Id); - manager1ViewModel.Employees[1].Manager.ShouldBeSameAs(manager1ViewModel); - } - - [Fact] - public void VerifyNestedSelfReference() - { - // Arrange - var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 }; - var manager3 = new Manager { Id = 101, EmployeeCode = "M003", Level = 100 }; - var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 }; - - var employee1 = new Employee { Id = 200, EmployeeCode = "E001"}; - var employee2 = new Employee { Id = 201, EmployeeCode = "E002"}; - var employee3 = new Employee { Id = 202, EmployeeCode = "E003"}; - - employee1.Manager = manager1; - employee2.Manager = manager2; - employee3.Manager = manager3; - - manager2.Manager = manager1; - manager3.Manager = manager2; - - // Act - var manager3ViewModel = manager3.ToManagerViewModel(); - - // Assert - manager3ViewModel.Manager.ShouldNotBeNull(); - manager3ViewModel.Manager.Id.ShouldBe(manager2.Id); - manager3ViewModel.Manager.Manager.Id.ShouldBe(manager1.Id); - manager3ViewModel.Employees.All(e => ReferenceEquals(e.Manager, manager3ViewModel)).ShouldBeTrue(); - } - } +using System.Linq; +using MapTo.Integration.Tests.Data.Models; +using MapTo.Integration.Tests.Data.ViewModels; +using Shouldly; +using Xunit; + +namespace MapTo.Integration.Tests +{ + public class CyclicReferenceTests + { + [Fact] + public void VerifySelfReference() + { + // Arrange + var manager = new Manager { Id = 1, EmployeeCode = "M001", Level = 100 }; + manager.Manager = manager; + + // Act + var result = manager.ToManagerViewModel(); + + // Assert + result.Id.ShouldBe(manager.Id); + result.EmployeeCode.ShouldBe(manager.EmployeeCode); + result.Level.ShouldBe(manager.Level); + result.Manager.ShouldBeSameAs(result); + } + + [Fact] + public void VerifyNestedReference() + { + // Arrange + var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 }; + var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 }; + + var employee1 = new Employee { Id = 200, EmployeeCode = "E001"}; + var employee2 = new Employee { Id = 201, EmployeeCode = "E002"}; + + employee1.Manager = manager1; + employee2.Manager = manager2; + + manager2.Manager = manager1; + + // Act + var manager1ViewModel = manager1.ToManagerViewModel(); + + // Assert + manager1ViewModel.Id.ShouldBe(manager1.Id); + manager1ViewModel.Manager.ShouldBeNull(); + manager1ViewModel.Employees.Count.ShouldBe(2); + manager1ViewModel.Employees[0].Id.ShouldBe(employee1.Id); + manager1ViewModel.Employees[0].Manager.ShouldBeSameAs(manager1ViewModel); + manager1ViewModel.Employees[1].Id.ShouldBe(manager2.Id); + manager1ViewModel.Employees[1].Manager.ShouldBeSameAs(manager1ViewModel); + } + + [Fact] + public void VerifyNestedSelfReference() + { + // Arrange + var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 }; + var manager3 = new Manager { Id = 101, EmployeeCode = "M003", Level = 100 }; + var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 }; + + var employee1 = new Employee { Id = 200, EmployeeCode = "E001"}; + var employee2 = new Employee { Id = 201, EmployeeCode = "E002"}; + var employee3 = new Employee { Id = 202, EmployeeCode = "E003"}; + + employee1.Manager = manager1; + employee2.Manager = manager2; + employee3.Manager = manager3; + + manager2.Manager = manager1; + manager3.Manager = manager2; + + // Act + var manager3ViewModel = manager3.ToManagerViewModel(); + + // Assert + manager3ViewModel.Manager.ShouldNotBeNull(); + manager3ViewModel.Manager.Id.ShouldBe(manager2.Id); + manager3ViewModel.Manager.Manager.Id.ShouldBe(manager1.Id); + manager3ViewModel.Employees.All(e => ReferenceEquals(e.Manager, manager3ViewModel)).ShouldBeTrue(); + } + } } \ No newline at end of file diff --git a/test/MapTo.Integration.Tests/Data/Models/Employee.cs b/test/MapTo.Integration.Tests/Data/Models/Employee.cs index 2944fc5..65a17fd 100644 --- a/test/MapTo.Integration.Tests/Data/Models/Employee.cs +++ b/test/MapTo.Integration.Tests/Data/Models/Employee.cs @@ -1,29 +1,29 @@ -namespace MapTo.Integration.Tests.Data.Models -{ - public class Employee - { - private Manager _manager; - - public int Id { get; set; } - - public string EmployeeCode { get; set; } - - public Manager Manager - { - get => _manager; - set - { - if (value == null) - { - _manager.Employees.Remove(this); - } - else - { - value.Employees.Add(this); - } - - _manager = value; - } - } - } +namespace MapTo.Integration.Tests.Data.Models +{ + public class Employee + { + private Manager _manager; + + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public Manager Manager + { + get => _manager; + set + { + if (value == null) + { + _manager.Employees.Remove(this); + } + else + { + value.Employees.Add(this); + } + + _manager = value; + } + } + } } \ No newline at end of file diff --git a/test/MapTo.Integration.Tests/Data/Models/Manager.cs b/test/MapTo.Integration.Tests/Data/Models/Manager.cs index ffafe25..3ad79fb 100644 --- a/test/MapTo.Integration.Tests/Data/Models/Manager.cs +++ b/test/MapTo.Integration.Tests/Data/Models/Manager.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; - -namespace MapTo.Integration.Tests.Data.Models -{ - public class Manager : Employee - { - public int Level { get; set; } - - public List Employees { get; set; } = new(); - } +using System.Collections.Generic; + +namespace MapTo.Integration.Tests.Data.Models +{ + public class Manager : Employee + { + public int Level { get; set; } + + public List Employees { get; set; } = new(); + } } \ No newline at end of file diff --git a/test/MapTo.Integration.Tests/Data/ViewModels/EmployeeViewModel.cs b/test/MapTo.Integration.Tests/Data/ViewModels/EmployeeViewModel.cs index 8ebaf88..424da90 100644 --- a/test/MapTo.Integration.Tests/Data/ViewModels/EmployeeViewModel.cs +++ b/test/MapTo.Integration.Tests/Data/ViewModels/EmployeeViewModel.cs @@ -1,14 +1,14 @@ -using MapTo.Integration.Tests.Data.Models; - -namespace MapTo.Integration.Tests.Data.ViewModels -{ - [MapFrom(typeof(Employee))] - public partial class EmployeeViewModel - { - public int Id { get; set; } - - public string EmployeeCode { get; set; } - - public ManagerViewModel Manager { get; set; } - } +using MapTo.Integration.Tests.Data.Models; + +namespace MapTo.Integration.Tests.Data.ViewModels +{ + [MapFrom(typeof(Employee))] + public partial class EmployeeViewModel + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public ManagerViewModel Manager { get; set; } + } } \ No newline at end of file diff --git a/test/MapTo.Integration.Tests/Data/ViewModels/ManagerViewModel.cs b/test/MapTo.Integration.Tests/Data/ViewModels/ManagerViewModel.cs index d085c24..63a2c9c 100644 --- a/test/MapTo.Integration.Tests/Data/ViewModels/ManagerViewModel.cs +++ b/test/MapTo.Integration.Tests/Data/ViewModels/ManagerViewModel.cs @@ -1,13 +1,13 @@ -using System.Collections.Generic; -using MapTo.Integration.Tests.Data.Models; - -namespace MapTo.Integration.Tests.Data.ViewModels -{ - [MapFrom(typeof(Manager))] - public partial class ManagerViewModel : EmployeeViewModel - { - public int Level { get; set; } - - public List Employees { get; set; } = new(); - } +using System.Collections.Generic; +using MapTo.Integration.Tests.Data.Models; + +namespace MapTo.Integration.Tests.Data.ViewModels +{ + [MapFrom(typeof(Manager))] + public partial class ManagerViewModel : EmployeeViewModel + { + public int Level { get; set; } + + public List Employees { get; set; } = new(); + } } \ No newline at end of file diff --git a/test/MapTo.Integration.Tests/MapTo.Integration.Tests.csproj b/test/MapTo.Integration.Tests/MapTo.Integration.Tests.csproj index f928846..57b5ca6 100644 --- a/test/MapTo.Integration.Tests/MapTo.Integration.Tests.csproj +++ b/test/MapTo.Integration.Tests/MapTo.Integration.Tests.csproj @@ -1,26 +1,26 @@ - - - - net5.0 - false - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - + + + + net5.0 + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/test/MapTo.Tests/Common.cs b/test/MapTo.Tests/Common.cs index f9113c9..34270e4 100644 --- a/test/MapTo.Tests/Common.cs +++ b/test/MapTo.Tests/Common.cs @@ -1,252 +1,252 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MapTo.Extensions; -using MapTo.Sources; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Shouldly; - -namespace MapTo.Tests -{ - internal static class Common - { - internal const int Indent1 = 4; - internal const int Indent2 = Indent1 * 2; - internal const int Indent3 = Indent1 * 3; - internal static readonly Location IgnoreLocation = Location.None; - - internal static readonly Dictionary DefaultAnalyzerOptions = new() - { - [GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" - }; - - internal static string GetSourceText(SourceGeneratorOptions? options = null) - { - const string ns = "Test"; - options ??= new SourceGeneratorOptions(); - var hasDifferentSourceNamespace = options.SourceClassNamespace != ns; - var builder = new SourceBuilder(); - - builder.WriteLine("//"); - builder.WriteLine("// Test source code."); - builder.WriteLine("//"); - builder.WriteLine(); - - options.Usings?.ForEach(s => builder.WriteLine($"using {s};")); - - if (options.UseMapToNamespace) - { - builder.WriteLine($"using {Constants.RootNamespace};"); - } - - builder - .WriteLine($"using {options.SourceClassNamespace};") - .WriteLine() - .WriteLine(); - - builder - .WriteLine($"namespace {ns}") - .WriteOpeningBracket(); - - if (hasDifferentSourceNamespace && options.UseMapToNamespace) - { - builder - .WriteLine($"using {options.SourceClassNamespace};") - .WriteLine() - .WriteLine(); - } - - builder - .WriteLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]") - .WriteLine("public partial class Foo") - .WriteOpeningBracket(); - - for (var i = 1; i <= options.ClassPropertiesCount; i++) - { - builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); - } - - options.PropertyBuilder?.Invoke(builder); - - builder - .WriteClosingBracket() - .WriteClosingBracket() - .WriteLine() - .WriteLine(); - - builder - .WriteLine($"namespace {options.SourceClassNamespace}") - .WriteOpeningBracket() - .WriteLine("public class Baz") - .WriteOpeningBracket(); - - for (var i = 1; i <= options.SourceClassPropertiesCount; i++) - { - builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); - } - - options.SourcePropertyBuilder?.Invoke(builder); - - builder - .WriteClosingBracket() - .WriteClosingBracket(); - - return builder.ToString(); - } - - internal static string[] GetEmployeeManagerSourceText( - Func? employeeClassSource = null, - Func? managerClassSource = null, - Func? employeeViewModelSource = null, - Func? managerViewModelSource = null, - bool useDifferentViewModelNamespace = false) - { - return new[] - { - employeeClassSource?.Invoke() ?? DefaultEmployeeClassSource(), - managerClassSource?.Invoke() ?? DefaultManagerClassSource(), - employeeViewModelSource?.Invoke() ?? - DefaultEmployeeViewModelSource(useDifferentViewModelNamespace), - managerViewModelSource?.Invoke() ?? DefaultManagerViewModelSource(useDifferentViewModelNamespace) - }; - - static string DefaultEmployeeClassSource() => - @" -using System; -using System.Collections.Generic; -using System.Text; - -namespace Test.Data.Models -{ - public class Employee - { - public int Id { get; set; } - - public string EmployeeCode { get; set; } - - public Manager Manager { get; set; } - } -}".Trim(); - - static string DefaultManagerClassSource() => - @"using System; -using System.Collections.Generic; -using System.Text; - -namespace Test.Data.Models -{ - public class Manager: Employee - { - public int Level { get; set; } - - public IEnumerable Employees { get; set; } = Array.Empty(); - } -} -".Trim(); - - static string DefaultEmployeeViewModelSource(bool useDifferentNamespace) => useDifferentNamespace - ? @" -using MapTo; -using Test.Data.Models; -using Test.ViewModels2; - -namespace Test.ViewModels -{ - [MapFrom(typeof(Employee))] - public partial class EmployeeViewModel - { - public int Id { get; set; } - - public string EmployeeCode { get; set; } - - public ManagerViewModel Manager { get; set; } - } -} -".Trim() - : @" -using MapTo; -using Test.Data.Models; - -namespace Test.ViewModels -{ - [MapFrom(typeof(Employee))] - public partial class EmployeeViewModel - { - public int Id { get; set; } - - public string EmployeeCode { get; set; } - - public ManagerViewModel Manager { get; set; } - } -} -".Trim(); - - static string DefaultManagerViewModelSource(bool useDifferentNamespace) => useDifferentNamespace - ? @" -using System; -using System.Collections.Generic; -using MapTo; -using Test.Data.Models; -using Test.ViewModels; - -namespace Test.ViewModels2 -{ - [MapFrom(typeof(Manager))] - public partial class ManagerViewModel : EmployeeViewModel - { - public int Level { get; set; } - - public IEnumerable Employees { get; set; } = Array.Empty(); - } -} -".Trim() - : @" -using System; -using System.Collections.Generic; -using MapTo; -using Test.Data.Models; - -namespace Test.ViewModels -{ - [MapFrom(typeof(Manager))] - public partial class ManagerViewModel : EmployeeViewModel - { - public int Level { get; set; } - - public IEnumerable Employees { get; set; } = Array.Empty(); - } -}".Trim(); - } - - internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo") - { - return syntaxTree.GetRoot() - .DescendantNodes() - .OfType() - .Single(c => c.Identifier.ValueText == targetClass) - .DescendantNodes() - .OfType() - .Single(p => p.Identifier.ValueText == targetPropertyName); - } - - internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo") - { - var syntaxTree = compilation.SyntaxTrees.First(); - var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass); - - var semanticModel = compilation.GetSemanticModel(syntaxTree); - return semanticModel.GetDeclaredSymbol(propSyntax).ShouldNotBeNull(); - } - - internal record SourceGeneratorOptions( - bool UseMapToNamespace = false, - string SourceClassNamespace = "Test.Models", - int ClassPropertiesCount = 3, - int SourceClassPropertiesCount = 3, - Action? PropertyBuilder = null, - Action? SourcePropertyBuilder = null, - IEnumerable? Usings = null); - } +using System; +using System.Collections.Generic; +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Shouldly; + +namespace MapTo.Tests +{ + internal static class Common + { + internal const int Indent1 = 4; + internal const int Indent2 = Indent1 * 2; + internal const int Indent3 = Indent1 * 3; + internal static readonly Location IgnoreLocation = Location.None; + + internal static readonly Dictionary DefaultAnalyzerOptions = new() + { + [GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" + }; + + internal static string GetSourceText(SourceGeneratorOptions? options = null) + { + const string ns = "Test"; + options ??= new SourceGeneratorOptions(); + var hasDifferentSourceNamespace = options.SourceClassNamespace != ns; + var builder = new SourceBuilder(); + + builder.WriteLine("//"); + builder.WriteLine("// Test source code."); + builder.WriteLine("//"); + builder.WriteLine(); + + options.Usings?.ForEach(s => builder.WriteLine($"using {s};")); + + if (options.UseMapToNamespace) + { + builder.WriteLine($"using {Constants.RootNamespace};"); + } + + builder + .WriteLine($"using {options.SourceClassNamespace};") + .WriteLine() + .WriteLine(); + + builder + .WriteLine($"namespace {ns}") + .WriteOpeningBracket(); + + if (hasDifferentSourceNamespace && options.UseMapToNamespace) + { + builder + .WriteLine($"using {options.SourceClassNamespace};") + .WriteLine() + .WriteLine(); + } + + builder + .WriteLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]") + .WriteLine("public partial class Foo") + .WriteOpeningBracket(); + + for (var i = 1; i <= options.ClassPropertiesCount; i++) + { + builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); + } + + options.PropertyBuilder?.Invoke(builder); + + builder + .WriteClosingBracket() + .WriteClosingBracket() + .WriteLine() + .WriteLine(); + + builder + .WriteLine($"namespace {options.SourceClassNamespace}") + .WriteOpeningBracket() + .WriteLine("public class Baz") + .WriteOpeningBracket(); + + for (var i = 1; i <= options.SourceClassPropertiesCount; i++) + { + builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); + } + + options.SourcePropertyBuilder?.Invoke(builder); + + builder + .WriteClosingBracket() + .WriteClosingBracket(); + + return builder.ToString(); + } + + internal static string[] GetEmployeeManagerSourceText( + Func? employeeClassSource = null, + Func? managerClassSource = null, + Func? employeeViewModelSource = null, + Func? managerViewModelSource = null, + bool useDifferentViewModelNamespace = false) + { + return new[] + { + employeeClassSource?.Invoke() ?? DefaultEmployeeClassSource(), + managerClassSource?.Invoke() ?? DefaultManagerClassSource(), + employeeViewModelSource?.Invoke() ?? + DefaultEmployeeViewModelSource(useDifferentViewModelNamespace), + managerViewModelSource?.Invoke() ?? DefaultManagerViewModelSource(useDifferentViewModelNamespace) + }; + + static string DefaultEmployeeClassSource() => + @" +using System; +using System.Collections.Generic; +using System.Text; + +namespace Test.Data.Models +{ + public class Employee + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public Manager Manager { get; set; } + } +}".Trim(); + + static string DefaultManagerClassSource() => + @"using System; +using System.Collections.Generic; +using System.Text; + +namespace Test.Data.Models +{ + public class Manager: Employee + { + public int Level { get; set; } + + public IEnumerable Employees { get; set; } = Array.Empty(); + } +} +".Trim(); + + static string DefaultEmployeeViewModelSource(bool useDifferentNamespace) => useDifferentNamespace + ? @" +using MapTo; +using Test.Data.Models; +using Test.ViewModels2; + +namespace Test.ViewModels +{ + [MapFrom(typeof(Employee))] + public partial class EmployeeViewModel + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public ManagerViewModel Manager { get; set; } + } +} +".Trim() + : @" +using MapTo; +using Test.Data.Models; + +namespace Test.ViewModels +{ + [MapFrom(typeof(Employee))] + public partial class EmployeeViewModel + { + public int Id { get; set; } + + public string EmployeeCode { get; set; } + + public ManagerViewModel Manager { get; set; } + } +} +".Trim(); + + static string DefaultManagerViewModelSource(bool useDifferentNamespace) => useDifferentNamespace + ? @" +using System; +using System.Collections.Generic; +using MapTo; +using Test.Data.Models; +using Test.ViewModels; + +namespace Test.ViewModels2 +{ + [MapFrom(typeof(Manager))] + public partial class ManagerViewModel : EmployeeViewModel + { + public int Level { get; set; } + + public IEnumerable Employees { get; set; } = Array.Empty(); + } +} +".Trim() + : @" +using System; +using System.Collections.Generic; +using MapTo; +using Test.Data.Models; + +namespace Test.ViewModels +{ + [MapFrom(typeof(Manager))] + public partial class ManagerViewModel : EmployeeViewModel + { + public int Level { get; set; } + + public IEnumerable Employees { get; set; } = Array.Empty(); + } +}".Trim(); + } + + internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo") + { + return syntaxTree.GetRoot() + .DescendantNodes() + .OfType() + .Single(c => c.Identifier.ValueText == targetClass) + .DescendantNodes() + .OfType() + .Single(p => p.Identifier.ValueText == targetPropertyName); + } + + internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo") + { + var syntaxTree = compilation.SyntaxTrees.First(); + var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass); + + var semanticModel = compilation.GetSemanticModel(syntaxTree); + return semanticModel.GetDeclaredSymbol(propSyntax).ShouldNotBeNull(); + } + + internal record SourceGeneratorOptions( + bool UseMapToNamespace = false, + string SourceClassNamespace = "Test.Models", + int ClassPropertiesCount = 3, + int SourceClassPropertiesCount = 3, + Action? PropertyBuilder = null, + Action? SourcePropertyBuilder = null, + IEnumerable? Usings = null); + } } \ No newline at end of file diff --git a/test/MapTo.Tests/CompilerServices/IsExternalInit.cs b/test/MapTo.Tests/CompilerServices/IsExternalInit.cs index e750e2f..72b53ee 100644 --- a/test/MapTo.Tests/CompilerServices/IsExternalInit.cs +++ b/test/MapTo.Tests/CompilerServices/IsExternalInit.cs @@ -1,16 +1,16 @@ -// ReSharper disable UnusedType.Global -// ReSharper disable CheckNamespace -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; - -namespace System.Runtime.CompilerServices -{ - /// - /// Reserved to be used by the compiler for tracking metadata. - /// This class should not be used by developers in source code. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit { } +// ReSharper disable UnusedType.Global +// ReSharper disable CheckNamespace +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { } } \ No newline at end of file diff --git a/test/MapTo.Tests/Extensions/RoslynExtensions.cs b/test/MapTo.Tests/Extensions/RoslynExtensions.cs index ea3394e..cddafeb 100644 --- a/test/MapTo.Tests/Extensions/RoslynExtensions.cs +++ b/test/MapTo.Tests/Extensions/RoslynExtensions.cs @@ -1,42 +1,42 @@ -using System; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; - -namespace MapTo.Tests.Extensions -{ - internal static class RoslynExtensions - { - internal static SyntaxTree? GetGeneratedSyntaxTree(this Compilation compilation, string className) => - compilation.SyntaxTrees.SingleOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs")); - - internal static string PrintSyntaxTree(this Compilation compilation) - { - var builder = new StringBuilder(); - - return string.Join( - Environment.NewLine, - compilation.SyntaxTrees - .Reverse() - .Select((s, i) => - { - builder - .Clear() - .AppendLine("----------------------------------------") - .AppendFormat("File Path: \"{0}\"", s.FilePath).AppendLine() - .AppendFormat("Index: \"{0}\"", i).AppendLine() - .AppendLine(); - - var lines = s.ToString().Split(Environment.NewLine); - var lineNumber = 0; - foreach (var line in lines) - { - builder.AppendFormat("{0:00}: {1}", lineNumber, line).AppendLine(); - lineNumber++; - } - - return builder.ToString(); - })); - } - } +using System; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace MapTo.Tests.Extensions +{ + internal static class RoslynExtensions + { + internal static SyntaxTree? GetGeneratedSyntaxTree(this Compilation compilation, string className) => + compilation.SyntaxTrees.SingleOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs")); + + internal static string PrintSyntaxTree(this Compilation compilation) + { + var builder = new StringBuilder(); + + return string.Join( + Environment.NewLine, + compilation.SyntaxTrees + .Reverse() + .Select((s, i) => + { + builder + .Clear() + .AppendLine("----------------------------------------") + .AppendFormat("File Path: \"{0}\"", s.FilePath).AppendLine() + .AppendFormat("Index: \"{0}\"", i).AppendLine() + .AppendLine(); + + var lines = s.ToString().Split(Environment.NewLine); + var lineNumber = 0; + foreach (var line in lines) + { + builder.AppendFormat("{0:00}: {1}", lineNumber, line).AppendLine(); + lineNumber++; + } + + return builder.ToString(); + })); + } + } } \ No newline at end of file diff --git a/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs b/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs index 1ad43d4..2b9dd87 100644 --- a/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs +++ b/test/MapTo.Tests/Extensions/ShouldlyExtensions.cs @@ -1,88 +1,88 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Shouldly; -using Xunit; - -namespace MapTo.Tests.Extensions -{ - internal static class ShouldlyExtensions - { - internal static void ShouldContainSource(this IEnumerable syntaxTree, string typeName, string expectedSource, string? customMessage = null) - { - var syntax = syntaxTree - .Select(s => s.ToString().Trim()) - .SingleOrDefault(s => s.Contains(typeName)); - - syntax.ShouldNotBeNullOrWhiteSpace(); - syntax.ShouldBe(expectedSource, customMessage); - } - - internal static void ShouldContainPartialSource(this IEnumerable syntaxTree, string typeName, string expectedSource, string? customMessage = null) - { - var syntax = syntaxTree - .Select(s => s.ToString().Trim()) - .SingleOrDefault(s => s.Contains(typeName)); - - syntax.ShouldNotBeNullOrWhiteSpace(); - syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage); - } - - internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string? customMessage = null) - { - var syntax = syntaxTree.ToString(); - syntax.ShouldNotBeNullOrWhiteSpace(); - syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage); - } - - internal static void ShouldBeSuccessful(this IEnumerable diagnostics, Compilation? compilation = null, IEnumerable? ignoreDiagnosticsIds = null) - { - var actual = diagnostics - .Where(d => (ignoreDiagnosticsIds is null || ignoreDiagnosticsIds.All(i => !d.Id.StartsWith(i) )) && (d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error)) - .Select(c => $"{c.Severity}: {c.Location.GetLineSpan()} - {c.GetMessage()}").ToArray(); - - if (!actual.Any()) - { - return; - } - - var builder = new StringBuilder(); - builder.AppendLine("Failed"); - - foreach (var d in actual) - { - builder.AppendFormat("- {0}", d).AppendLine(); - } - - if (compilation is not null) - { - builder.AppendLine("Generated Sources:"); - builder.AppendLine(compilation.PrintSyntaxTree()); - } - - Assert.False(true, builder.ToString()); - } - - internal static void ShouldNotBeSuccessful(this ImmutableArray diagnostics, Diagnostic expectedError) - { - var actualDiagnostics = diagnostics.SingleOrDefault(d => d.Id == expectedError.Id); - var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics }); - - compilationDiagnostics.ShouldBeSuccessful(); - - Assert.NotNull(actualDiagnostics); - Assert.Equal(expectedError.Id, actualDiagnostics?.Id); - Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics?.Descriptor.Id); - Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics?.Descriptor.Description); - Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics?.Descriptor.Title); - - if (expectedError.Location != Location.None) - { - Assert.Equal(expectedError.Location, actualDiagnostics?.Location); - } - } - } +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Shouldly; +using Xunit; + +namespace MapTo.Tests.Extensions +{ + internal static class ShouldlyExtensions + { + internal static void ShouldContainSource(this IEnumerable syntaxTree, string typeName, string expectedSource, string? customMessage = null) + { + var syntax = syntaxTree + .Select(s => s.ToString().Trim()) + .SingleOrDefault(s => s.Contains(typeName)); + + syntax.ShouldNotBeNullOrWhiteSpace(); + syntax.ShouldBe(expectedSource, customMessage); + } + + internal static void ShouldContainPartialSource(this IEnumerable syntaxTree, string typeName, string expectedSource, string? customMessage = null) + { + var syntax = syntaxTree + .Select(s => s.ToString().Trim()) + .SingleOrDefault(s => s.Contains(typeName)); + + syntax.ShouldNotBeNullOrWhiteSpace(); + syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage); + } + + internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string? customMessage = null) + { + var syntax = syntaxTree.ToString(); + syntax.ShouldNotBeNullOrWhiteSpace(); + syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage); + } + + internal static void ShouldBeSuccessful(this IEnumerable diagnostics, Compilation? compilation = null, IEnumerable? ignoreDiagnosticsIds = null) + { + var actual = diagnostics + .Where(d => (ignoreDiagnosticsIds is null || ignoreDiagnosticsIds.All(i => !d.Id.StartsWith(i) )) && (d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error)) + .Select(c => $"{c.Severity}: {c.Location.GetLineSpan()} - {c.GetMessage()}").ToArray(); + + if (!actual.Any()) + { + return; + } + + var builder = new StringBuilder(); + builder.AppendLine("Failed"); + + foreach (var d in actual) + { + builder.AppendFormat("- {0}", d).AppendLine(); + } + + if (compilation is not null) + { + builder.AppendLine("Generated Sources:"); + builder.AppendLine(compilation.PrintSyntaxTree()); + } + + Assert.False(true, builder.ToString()); + } + + internal static void ShouldNotBeSuccessful(this ImmutableArray diagnostics, Diagnostic expectedError) + { + var actualDiagnostics = diagnostics.SingleOrDefault(d => d.Id == expectedError.Id); + var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics }); + + compilationDiagnostics.ShouldBeSuccessful(); + + Assert.NotNull(actualDiagnostics); + Assert.Equal(expectedError.Id, actualDiagnostics?.Id); + Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics?.Descriptor.Id); + Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics?.Descriptor.Description); + Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics?.Descriptor.Title); + + if (expectedError.Location != Location.None) + { + Assert.Equal(expectedError.Location, actualDiagnostics?.Location); + } + } + } } \ No newline at end of file diff --git a/test/MapTo.Tests/IgnorePropertyAttributeTests.cs b/test/MapTo.Tests/IgnorePropertyAttributeTests.cs index a1dddb6..cd20bef 100644 --- a/test/MapTo.Tests/IgnorePropertyAttributeTests.cs +++ b/test/MapTo.Tests/IgnorePropertyAttributeTests.cs @@ -1,79 +1,79 @@ -using System.Linq; -using MapTo.Extensions; -using MapTo.Sources; -using MapTo.Tests.Extensions; -using MapTo.Tests.Infrastructure; -using Shouldly; -using Xunit; -using static MapTo.Tests.Common; - -namespace MapTo.Tests -{ - public class IgnorePropertyAttributeTests - { - [Fact] - public void VerifyIgnorePropertyAttribute() - { - // Arrange - const string source = ""; - var expectedAttribute = $@" -{Constants.GeneratedFilesHeader} -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class IgnorePropertyAttribute : Attribute {{ }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute); - } - - [Fact] - public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .WriteLine("[IgnoreProperty]") - .WriteLine("public int Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); - - var expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Models.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - } -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); - } - } +using System.Linq; +using MapTo.Extensions; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Shouldly; +using Xunit; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class IgnorePropertyAttributeTests + { + [Fact] + public void VerifyIgnorePropertyAttribute() + { + // Arrange + const string source = ""; + var expectedAttribute = $@" +{Constants.GeneratedFilesHeader} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class IgnorePropertyAttribute : Attribute {{ }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute); + } + + [Fact] + public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .WriteLine("[IgnoreProperty]") + .WriteLine("public int Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Models.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } + } } \ No newline at end of file diff --git a/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs b/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs index da1ec81..872f37c 100644 --- a/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs +++ b/test/MapTo.Tests/Infrastructure/CSharpGenerator.cs @@ -1,64 +1,64 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using MapTo.Tests.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -namespace MapTo.Tests.Infrastructure -{ - internal static class CSharpGenerator - { - internal static (Compilation compilation, ImmutableArray diagnostics) GetOutputCompilation( - string source, - bool assertCompilation = false, - IDictionary? analyzerConfigOptions = null, - NullableContextOptions nullableContextOptions = NullableContextOptions.Disable, - LanguageVersion languageVersion = LanguageVersion.CSharp7_3) => - GetOutputCompilation( - new[] { source }, - assertCompilation, - analyzerConfigOptions, - nullableContextOptions, - languageVersion); - - internal static (Compilation compilation, ImmutableArray diagnostics) GetOutputCompilation( - IEnumerable sources, - bool assertCompilation = false, - IDictionary? analyzerConfigOptions = null, - NullableContextOptions nullableContextOptions = NullableContextOptions.Disable, - LanguageVersion languageVersion = LanguageVersion.CSharp7_3) - { - var references = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location)) - .Select(a => MetadataReference.CreateFromFile(a.Location)) - .ToList(); - - var compilation = CSharpCompilation.Create( - $"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", - sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs", options: new CSharpParseOptions(languageVersion))), - references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions)); - - if (assertCompilation) - { - // NB: fail tests when the injected program isn't valid _before_ running generators - compilation.GetDiagnostics().ShouldBeSuccessful(); - } - - var driver = CSharpGeneratorDriver.Create( - new[] { new MapToGenerator() }, - optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions), - parseOptions: new CSharpParseOptions(languageVersion) - ); - - driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics); - - generateDiagnostics.ShouldBeSuccessful(ignoreDiagnosticsIds: new[] { "MT" }); - outputCompilation.GetDiagnostics().ShouldBeSuccessful(outputCompilation); - - return (outputCompilation, generateDiagnostics); - } - } +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using MapTo.Tests.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace MapTo.Tests.Infrastructure +{ + internal static class CSharpGenerator + { + internal static (Compilation compilation, ImmutableArray diagnostics) GetOutputCompilation( + string source, + bool assertCompilation = false, + IDictionary? analyzerConfigOptions = null, + NullableContextOptions nullableContextOptions = NullableContextOptions.Disable, + LanguageVersion languageVersion = LanguageVersion.CSharp7_3) => + GetOutputCompilation( + new[] { source }, + assertCompilation, + analyzerConfigOptions, + nullableContextOptions, + languageVersion); + + internal static (Compilation compilation, ImmutableArray diagnostics) GetOutputCompilation( + IEnumerable sources, + bool assertCompilation = false, + IDictionary? analyzerConfigOptions = null, + NullableContextOptions nullableContextOptions = NullableContextOptions.Disable, + LanguageVersion languageVersion = LanguageVersion.CSharp7_3) + { + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location)) + .Select(a => MetadataReference.CreateFromFile(a.Location)) + .ToList(); + + var compilation = CSharpCompilation.Create( + $"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", + sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs", options: new CSharpParseOptions(languageVersion))), + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions)); + + if (assertCompilation) + { + // NB: fail tests when the injected program isn't valid _before_ running generators + compilation.GetDiagnostics().ShouldBeSuccessful(); + } + + var driver = CSharpGeneratorDriver.Create( + new[] { new MapToGenerator() }, + optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions), + parseOptions: new CSharpParseOptions(languageVersion) + ); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics); + + generateDiagnostics.ShouldBeSuccessful(ignoreDiagnosticsIds: new[] { "MT" }); + outputCompilation.GetDiagnostics().ShouldBeSuccessful(outputCompilation); + + return (outputCompilation, generateDiagnostics); + } + } } \ No newline at end of file diff --git a/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptions.cs b/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptions.cs index 462d71d..5cfa68f 100644 --- a/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptions.cs +++ b/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptions.cs @@ -1,19 +1,19 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace MapTo.Tests.Infrastructure -{ - internal sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions - { - private readonly ImmutableDictionary _backing; - - public TestAnalyzerConfigOptions(IDictionary? properties) - { - _backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create(KeyComparer); - } - - public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value); - } +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace MapTo.Tests.Infrastructure +{ + internal sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions + { + private readonly ImmutableDictionary _backing; + + public TestAnalyzerConfigOptions(IDictionary? properties) + { + _backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create(KeyComparer); + } + + public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value); + } } \ No newline at end of file diff --git a/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptionsProvider.cs b/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptionsProvider.cs index 914901d..9c80539 100644 --- a/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptionsProvider.cs +++ b/test/MapTo.Tests/Infrastructure/TestAnalyzerConfigOptionsProvider.cs @@ -1,24 +1,24 @@ -using System; -using System.Collections.Generic; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace MapTo.Tests.Infrastructure -{ - internal sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider - { - public TestAnalyzerConfigOptionsProvider(IDictionary? options) - { - GlobalOptions = new TestAnalyzerConfigOptions(options); - } - - /// - public override AnalyzerConfigOptions GlobalOptions { get; } - - /// - public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); - - /// - public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException(); - } +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace MapTo.Tests.Infrastructure +{ + internal sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider + { + public TestAnalyzerConfigOptionsProvider(IDictionary? options) + { + GlobalOptions = new TestAnalyzerConfigOptions(options); + } + + /// + public override AnalyzerConfigOptions GlobalOptions { get; } + + /// + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); + + /// + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/test/MapTo.Tests/MapPropertyTests.cs b/test/MapTo.Tests/MapPropertyTests.cs index 54af284..ced2108 100644 --- a/test/MapTo.Tests/MapPropertyTests.cs +++ b/test/MapTo.Tests/MapPropertyTests.cs @@ -1,202 +1,202 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MapTo.Sources; -using MapTo.Tests.Extensions; -using MapTo.Tests.Infrastructure; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Xunit; -using static MapTo.Tests.Common; - -namespace MapTo.Tests -{ - public class MapPropertyTests - { - [Theory] - [InlineData(NullableContextOptions.Disable)] - [InlineData(NullableContextOptions.Enable)] - public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions) - { - // Arrange - const string source = ""; - var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty; - var languageVersion = nullableContextOptions == NullableContextOptions.Enable ? LanguageVersion.CSharp8 : LanguageVersion.CSharp7_3; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} -{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)} -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] - public sealed class MapPropertyAttribute : Attribute - {{ - public string{nullableSyntax} SourcePropertyName {{ get; set; }} - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions, languageVersion: languageVersion); - - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface); - } - - [Fact] - public void When_MapPropertyFound_Should_UseItToMapToSourceProperty() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .WriteLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]") - .WriteLine("public int Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); - - var expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Models.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - Prop4 = baz.Prop3; - } -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); - } - - [Theory] - [MemberData(nameof(MapPropertyWithImplicitConversionFoundData))] - public void When_MapPropertyWithImplicitConversionFound_Should_UseItToMapToSourceProperty(string source, string expectedResult, LanguageVersion languageVersion) - { - // Arrange - source = source.Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); - } - - public static IEnumerable MapPropertyWithImplicitConversionFoundData => new List - { - new object[] - { - @" -namespace Test -{ - using System.Collections.Generic; - - public class InnerClass { public int Prop1 { get; set; } } - public class OuterClass - { - public int Id { get; set; } - public List InnerProp { get; set; } - } -} - -namespace Test.Models -{ - using MapTo; - using System.Collections.Generic; - - [MapFrom(typeof(Test.InnerClass))] - public partial class InnerClass { public int Prop1 { get; set; } } - - [MapFrom(typeof(Test.OuterClass))] - public partial class OuterClass - { - public int Id { get; set; } - public IReadOnlyList InnerProp { get; set; } - } -} -", - @" - private protected OuterClass(MappingContext context, Test.OuterClass outerClass) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (outerClass == null) throw new ArgumentNullException(nameof(outerClass)); - - context.Register(outerClass, this); - - Id = outerClass.Id; - InnerProp = outerClass.InnerProp.Select(context.MapFromWithContext).ToList(); - } -", - LanguageVersion.CSharp7_3 - }, - new object[] - { - @" -namespace Test -{ - using System; - using System.Collections.Generic; - - public class InnerClass - { - public int Id { get; set; } - public string Name { get; set; } - } - - public class OuterClass - { - public int Id { get; set; } - public List InnerClasses { get; set; } - public DateTime? SomeDate { get; set; } - } -} - -namespace Test.Models -{ - using MapTo; - using System; - using System.Collections.Generic; - - [MapFrom(typeof(Test.InnerClass))] - public partial record InnerClass(int Id, string Name); - - [MapFrom(typeof(Test.OuterClass))] - public partial record OuterClass(int Id, DateTime? SomeDate, IReadOnlyList InnerClasses); -} -", - @" - private protected OuterClass(MappingContext context, Test.OuterClass outerClass) - : this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext).ToList()) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (outerClass == null) throw new ArgumentNullException(nameof(outerClass)); - - context.Register(outerClass, this); - } -", - LanguageVersion.CSharp9 - } - }; - } +using System; +using System.Collections.Generic; +using System.Linq; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MapPropertyTests + { + [Theory] + [InlineData(NullableContextOptions.Disable)] + [InlineData(NullableContextOptions.Enable)] + public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions) + { + // Arrange + const string source = ""; + var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty; + var languageVersion = nullableContextOptions == NullableContextOptions.Enable ? LanguageVersion.CSharp8 : LanguageVersion.CSharp7_3; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] + public sealed class MapPropertyAttribute : Attribute + {{ + public string{nullableSyntax} SourcePropertyName {{ get; set; }} + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions, languageVersion: languageVersion); + + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface); + } + + [Fact] + public void When_MapPropertyFound_Should_UseItToMapToSourceProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .WriteLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]") + .WriteLine("public int Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Models.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + Prop4 = baz.Prop3; + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } + + [Theory] + [MemberData(nameof(MapPropertyWithImplicitConversionFoundData))] + public void When_MapPropertyWithImplicitConversionFound_Should_UseItToMapToSourceProperty(string source, string expectedResult, LanguageVersion languageVersion) + { + // Arrange + source = source.Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } + + public static IEnumerable MapPropertyWithImplicitConversionFoundData => new List + { + new object[] + { + @" +namespace Test +{ + using System.Collections.Generic; + + public class InnerClass { public int Prop1 { get; set; } } + public class OuterClass + { + public int Id { get; set; } + public List InnerProp { get; set; } + } +} + +namespace Test.Models +{ + using MapTo; + using System.Collections.Generic; + + [MapFrom(typeof(Test.InnerClass))] + public partial class InnerClass { public int Prop1 { get; set; } } + + [MapFrom(typeof(Test.OuterClass))] + public partial class OuterClass + { + public int Id { get; set; } + public IReadOnlyList InnerProp { get; set; } + } +} +", + @" + private protected OuterClass(MappingContext context, Test.OuterClass outerClass) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (outerClass == null) throw new ArgumentNullException(nameof(outerClass)); + + context.Register(outerClass, this); + + Id = outerClass.Id; + InnerProp = outerClass.InnerProp.Select(context.MapFromWithContext).ToList(); + } +", + LanguageVersion.CSharp7_3 + }, + new object[] + { + @" +namespace Test +{ + using System; + using System.Collections.Generic; + + public class InnerClass + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class OuterClass + { + public int Id { get; set; } + public List InnerClasses { get; set; } + public DateTime? SomeDate { get; set; } + } +} + +namespace Test.Models +{ + using MapTo; + using System; + using System.Collections.Generic; + + [MapFrom(typeof(Test.InnerClass))] + public partial record InnerClass(int Id, string Name); + + [MapFrom(typeof(Test.OuterClass))] + public partial record OuterClass(int Id, DateTime? SomeDate, IReadOnlyList InnerClasses); +} +", + @" + private protected OuterClass(MappingContext context, Test.OuterClass outerClass) + : this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext).ToList()) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (outerClass == null) throw new ArgumentNullException(nameof(outerClass)); + + context.Register(outerClass, this); + } +", + LanguageVersion.CSharp9 + } + }; + } } \ No newline at end of file diff --git a/test/MapTo.Tests/MapToTests.cs b/test/MapTo.Tests/MapToTests.cs index e09009c..bab362b 100644 --- a/test/MapTo.Tests/MapToTests.cs +++ b/test/MapTo.Tests/MapToTests.cs @@ -1,601 +1,601 @@ -using System.Collections.Generic; -using System.Linq; -using MapTo.Sources; -using MapTo.Tests.Extensions; -using MapTo.Tests.Infrastructure; -using Shouldly; -using Xunit; -using static MapTo.Extensions.GeneratorExecutionContextExtensions; -using static MapTo.Tests.Common; - -namespace MapTo.Tests -{ - public class MapToTests - { - private static readonly string ExpectedAttribute = $@"{Constants.GeneratedFilesHeader} -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class MapFromAttribute : Attribute - {{ - public MapFromAttribute(Type sourceType) - {{ - SourceType = sourceType; - }} - - public Type SourceType {{ get; }} - }} -}}"; - - [Fact] - public void VerifyMapToAttribute() - { - // Arrange - const string source = ""; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeClassName, ExpectedAttribute); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithDifferentTypes_Should_ReportError() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => { builder.WriteLine("public string Prop4 { get; set; }"); }, - SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation)); - - diagnostics.ShouldNotBeSuccessful(expectedError); - } - - [Fact] - public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier() - { - // Arrange - var source = GetSourceText(); - var configOptions = new Dictionary - { - [GetBuildPropertyName(nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier))] = "Internal", - [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" - }; - - var expectedExtension = @" - internal static partial class BazToFooExtensions - { - internal static Foo ToFoo(this Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - } - }".Trim(); - - var expectedFactory = @" - internal static Foo From(Test.Models.Baz baz) - { - return baz == null ? null : MappingContext.Create(baz); - }".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - - var syntaxTree = compilation.SyntaxTrees.Last().ToString(); - syntaxTree.ShouldContain(expectedFactory); - syntaxTree.ShouldContain(expectedExtension); - } - - [Fact] - public void When_MapToAttributeFound_Should_GenerateTheClass() - { - // Arrange - const string source = @" -using MapTo; - -namespace Test -{ - [MapFrom(typeof(Baz))] - public partial class Foo - { - public int Prop1 { get; set; } - } - - public class Baz - { - public int Prop1 { get; set; } - } -} -"; - - const string expectedResult = @" -// -using MapTo; -using System; - -namespace Test -{ - partial class Foo - { - public Foo(Test.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); - } - - [Fact] - public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError() - { - // Arrange - const string source = @" -using MapTo; - -namespace Test -{ - [MapFrom(typeof(Baz))] - public partial class Foo { } - - public class Baz { public int Prop1 { get; set; } } -} -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - var fooType = compilation.GetTypeByMetadataName("Test.Foo"); - fooType.ShouldNotBeNull(); - - var bazType = compilation.GetTypeByMetadataName("Test.Baz"); - bazType.ShouldNotBeNull(); - - var expectedDiagnostic = DiagnosticsFactory.NoMatchingPropertyFoundError(fooType.Locations.Single(), fooType, bazType); - var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id); - error.ShouldNotBeNull(); - } - - [Fact] - public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass() - { - // Arrange - const string source = @" -namespace Test -{ - [MapTo.MapFrom(typeof(Baz))] - public partial class Foo { public int Prop1 { get; set; } } - - public class Baz { public int Prop1 { get; set; } } -} -"; - - const string expectedResult = @" -// -using MapTo; -using System; - -namespace Test -{ - partial class Foo - { - public Foo(Test.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); - } - - [Fact] - public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute() - { - // Arrange - const string source = ""; - var expectedTypes = new[] - { - IgnorePropertyAttributeSource.AttributeName, - MapFromAttributeSource.AttributeName, - ITypeConverterSource.InterfaceName, - MapPropertyAttributeSource.AttributeName - }; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees - .Select(s => s.ToString()) - .Where(s => !string.IsNullOrWhiteSpace(s.ToString())) - .All(s => expectedTypes.Any(s.Contains)) - .ShouldBeTrue(); - } - - [Fact] - public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar")); - - const string expectedResult = @" -// -using MapTo; -using System; - -namespace Test -{ -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); - } - - [Fact] - public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest() - { - // Arrange - var source = GetSourceText(); - - const string expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Models.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); - } - - [Fact] - public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod() - { - // Arrange - var source = GetSourceText(); - - const string expectedResult = @" - public static Foo From(Test.Models.Baz baz) - { - return baz == null ? null : MappingContext.Create(baz); - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); - } - - [Fact] - public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType() - { - // Arrange - var source = GetSourceText(); - - const string expectedResult = @" - public static partial class BazToFooExtensions - { - public static Foo ToFoo(this Test.Models.Baz baz) - { - return baz == null ? null : new Foo(baz); - } - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); - } - - [Fact] - public void When_HasNestedObjectPropertyTypeHasMapFromAttribute_Should_UseContinueToMap() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - SourceClassNamespace: "Test", - PropertyBuilder: b => b.WriteLine("public B InnerProp1 { get; }"), - SourcePropertyBuilder: b => b.WriteLine("public A InnerProp1 { get; }"))); - - source += @" -namespace Test -{ - public class A { public int Prop1 { get; } } - - [MapTo.MapFrom(typeof(A))] - public partial class B { public int Prop1 { get; }} -} -".Trim(); - - var expectedResult = @" - partial class Foo - { - public Foo(Test.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - InnerProp1 = context.MapFromWithContext(baz.InnerProp1); - } -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ToArray()[^2].ShouldContainPartialSource(expectedResult); - } - - [Fact] - public void When_HasNestedObjectPropertyTypeDoesNotHaveMapFromAttribute_Should_ReportError() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - SourceClassNamespace: "Test", - PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"), - SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }"))); - - source += @" -namespace Test -{ - public class FooInner1 { public int Prop1 { get; } } - - public partial class BazInner1 { public int Prop1 { get; }} -} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation)); - diagnostics.ShouldNotBeSuccessful(expectedError); - } - - [Fact] - public void When_HasNestedObjectPropertyTypeHasMapFromAttributeToDifferentType_Should_ReportError() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - SourceClassNamespace: "Test", - PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"), - SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }"))); - - source += @" -namespace Test -{ - public class FooInner1 { public int Prop1 { get; } } - - public class FooInner2 { public int Prop1 { get; } } - - [MapTo.MapFrom(typeof(FooInner2))] - public partial class BazInner1 { public int Prop1 { get; }} -} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation)); - diagnostics.ShouldNotBeSuccessful(expectedError); - } - - [Fact] - public void When_SourceTypeEnumerableProperties_Should_CreateConstructorAndAssignSrcToDest() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - Usings: new[] { "System.Collections.Generic"}, - PropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }"), - SourcePropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }"))); - - const string expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Models.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - Prop4 = baz.Prop4; - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); - } - - [Fact] - public void When_DestinationTypeHasBaseClass_Should_CallBaseConstructor() - { - // Arrange - var sources = GetEmployeeManagerSourceText(); - - const string expectedResult = @" -private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager) -{ - if (context == null) throw new ArgumentNullException(nameof(context)); - if (manager == null) throw new ArgumentNullException(nameof(manager)); - - context.Register(manager, this); - - Level = manager.Level; - Employees = manager.Employees.Select(context.MapFromWithContext).ToList(); -} -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); - } - - [Fact] - public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttribute_Should_CreateANewEnumerableWithMappedObjects() - { - // Arrange - var sources = GetEmployeeManagerSourceText(); - - const string expectedResult = @" -// -using MapTo; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Test.ViewModels -{ - partial class ManagerViewModel - { - public ManagerViewModel(Test.Data.Models.Manager manager) - : this(new MappingContext(), manager) { } - - private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (manager == null) throw new ArgumentNullException(nameof(manager)); - - context.Register(manager, this); - - Level = manager.Level; - Employees = manager.Employees.Select(context.MapFromWithContext).ToList(); - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); - } - - [Fact] - public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttributeInDifferentNamespaces_Should_CreateANewEnumerableWithMappedObjectsAndImportNamespace() - { - // Arrange - var sources = GetEmployeeManagerSourceText(useDifferentViewModelNamespace: true); - - const string expectedResult = @" -using MapTo; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Test.ViewModels2 -{ - partial class ManagerViewModel - { - public ManagerViewModel(Test.Data.Models.Manager manager) - : this(new MappingContext(), manager) { } - - private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (manager == null) throw new ArgumentNullException(nameof(manager)); - - context.Register(manager, this); - - Level = manager.Level; - Employees = manager.Employees.Select(context.MapFromWithContext).ToList(); - } -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); - } - } +using System.Collections.Generic; +using System.Linq; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Shouldly; +using Xunit; +using static MapTo.Extensions.GeneratorExecutionContextExtensions; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MapToTests + { + private static readonly string ExpectedAttribute = $@"{Constants.GeneratedFilesHeader} +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class MapFromAttribute : Attribute + {{ + public MapFromAttribute(Type sourceType) + {{ + SourceType = sourceType; + }} + + public Type SourceType {{ get; }} + }} +}}"; + + [Fact] + public void VerifyMapToAttribute() + { + // Arrange + const string source = ""; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeClassName, ExpectedAttribute); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithDifferentTypes_Should_ReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => { builder.WriteLine("public string Prop4 { get; set; }"); }, + SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation)); + + diagnostics.ShouldNotBeSuccessful(expectedError); + } + + [Fact] + public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier() + { + // Arrange + var source = GetSourceText(); + var configOptions = new Dictionary + { + [GetBuildPropertyName(nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier))] = "Internal", + [GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" + }; + + var expectedExtension = @" + internal static partial class BazToFooExtensions + { + internal static Foo ToFoo(this Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + } + }".Trim(); + + var expectedFactory = @" + internal static Foo From(Test.Models.Baz baz) + { + return baz == null ? null : MappingContext.Create(baz); + }".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + + var syntaxTree = compilation.SyntaxTrees.Last().ToString(); + syntaxTree.ShouldContain(expectedFactory); + syntaxTree.ShouldContain(expectedExtension); + } + + [Fact] + public void When_MapToAttributeFound_Should_GenerateTheClass() + { + // Arrange + const string source = @" +using MapTo; + +namespace Test +{ + [MapFrom(typeof(Baz))] + public partial class Foo + { + public int Prop1 { get; set; } + } + + public class Baz + { + public int Prop1 { get; set; } + } +} +"; + + const string expectedResult = @" +// +using MapTo; +using System; + +namespace Test +{ + partial class Foo + { + public Foo(Test.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); + } + + [Fact] + public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError() + { + // Arrange + const string source = @" +using MapTo; + +namespace Test +{ + [MapFrom(typeof(Baz))] + public partial class Foo { } + + public class Baz { public int Prop1 { get; set; } } +} +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + var fooType = compilation.GetTypeByMetadataName("Test.Foo"); + fooType.ShouldNotBeNull(); + + var bazType = compilation.GetTypeByMetadataName("Test.Baz"); + bazType.ShouldNotBeNull(); + + var expectedDiagnostic = DiagnosticsFactory.NoMatchingPropertyFoundError(fooType.Locations.Single(), fooType, bazType); + var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id); + error.ShouldNotBeNull(); + } + + [Fact] + public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass() + { + // Arrange + const string source = @" +namespace Test +{ + [MapTo.MapFrom(typeof(Baz))] + public partial class Foo { public int Prop1 { get; set; } } + + public class Baz { public int Prop1 { get; set; } } +} +"; + + const string expectedResult = @" +// +using MapTo; +using System; + +namespace Test +{ + partial class Foo + { + public Foo(Test.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); + } + + [Fact] + public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute() + { + // Arrange + const string source = ""; + var expectedTypes = new[] + { + IgnorePropertyAttributeSource.AttributeName, + MapFromAttributeSource.AttributeName, + ITypeConverterSource.InterfaceName, + MapPropertyAttributeSource.AttributeName + }; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees + .Select(s => s.ToString()) + .Where(s => !string.IsNullOrWhiteSpace(s.ToString())) + .All(s => expectedTypes.Any(s.Contains)) + .ShouldBeTrue(); + } + + [Fact] + public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar")); + + const string expectedResult = @" +// +using MapTo; +using System; + +namespace Test +{ +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim()); + } + + [Fact] + public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest() + { + // Arrange + var source = GetSourceText(); + + const string expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Models.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); + } + + [Fact] + public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod() + { + // Arrange + var source = GetSourceText(); + + const string expectedResult = @" + public static Foo From(Test.Models.Baz baz) + { + return baz == null ? null : MappingContext.Create(baz); + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); + } + + [Fact] + public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType() + { + // Arrange + var source = GetSourceText(); + + const string expectedResult = @" + public static partial class BazToFooExtensions + { + public static Foo ToFoo(this Test.Models.Baz baz) + { + return baz == null ? null : new Foo(baz); + } + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); + } + + [Fact] + public void When_HasNestedObjectPropertyTypeHasMapFromAttribute_Should_UseContinueToMap() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + SourceClassNamespace: "Test", + PropertyBuilder: b => b.WriteLine("public B InnerProp1 { get; }"), + SourcePropertyBuilder: b => b.WriteLine("public A InnerProp1 { get; }"))); + + source += @" +namespace Test +{ + public class A { public int Prop1 { get; } } + + [MapTo.MapFrom(typeof(A))] + public partial class B { public int Prop1 { get; }} +} +".Trim(); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + InnerProp1 = context.MapFromWithContext(baz.InnerProp1); + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ToArray()[^2].ShouldContainPartialSource(expectedResult); + } + + [Fact] + public void When_HasNestedObjectPropertyTypeDoesNotHaveMapFromAttribute_Should_ReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + SourceClassNamespace: "Test", + PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"), + SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }"))); + + source += @" +namespace Test +{ + public class FooInner1 { public int Prop1 { get; } } + + public partial class BazInner1 { public int Prop1 { get; }} +} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation)); + diagnostics.ShouldNotBeSuccessful(expectedError); + } + + [Fact] + public void When_HasNestedObjectPropertyTypeHasMapFromAttributeToDifferentType_Should_ReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + SourceClassNamespace: "Test", + PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"), + SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }"))); + + source += @" +namespace Test +{ + public class FooInner1 { public int Prop1 { get; } } + + public class FooInner2 { public int Prop1 { get; } } + + [MapTo.MapFrom(typeof(FooInner2))] + public partial class BazInner1 { public int Prop1 { get; }} +} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation)); + diagnostics.ShouldNotBeSuccessful(expectedError); + } + + [Fact] + public void When_SourceTypeEnumerableProperties_Should_CreateConstructorAndAssignSrcToDest() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + Usings: new[] { "System.Collections.Generic"}, + PropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }"), + SourcePropertyBuilder: builder => builder.WriteLine("public IEnumerable Prop4 { get; }"))); + + const string expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Models.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + Prop4 = baz.Prop4; + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim()); + } + + [Fact] + public void When_DestinationTypeHasBaseClass_Should_CallBaseConstructor() + { + // Arrange + var sources = GetEmployeeManagerSourceText(); + + const string expectedResult = @" +private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager) +{ + if (context == null) throw new ArgumentNullException(nameof(context)); + if (manager == null) throw new ArgumentNullException(nameof(manager)); + + context.Register(manager, this); + + Level = manager.Level; + Employees = manager.Employees.Select(context.MapFromWithContext).ToList(); +} +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } + + [Fact] + public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttribute_Should_CreateANewEnumerableWithMappedObjects() + { + // Arrange + var sources = GetEmployeeManagerSourceText(); + + const string expectedResult = @" +// +using MapTo; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Test.ViewModels +{ + partial class ManagerViewModel + { + public ManagerViewModel(Test.Data.Models.Manager manager) + : this(new MappingContext(), manager) { } + + private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (manager == null) throw new ArgumentNullException(nameof(manager)); + + context.Register(manager, this); + + Level = manager.Level; + Employees = manager.Employees.Select(context.MapFromWithContext).ToList(); + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } + + [Fact] + public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttributeInDifferentNamespaces_Should_CreateANewEnumerableWithMappedObjectsAndImportNamespace() + { + // Arrange + var sources = GetEmployeeManagerSourceText(useDifferentViewModelNamespace: true); + + const string expectedResult = @" +using MapTo; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Test.ViewModels2 +{ + partial class ManagerViewModel + { + public ManagerViewModel(Test.Data.Models.Manager manager) + : this(new MappingContext(), manager) { } + + private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (manager == null) throw new ArgumentNullException(nameof(manager)); + + context.Register(manager, this); + + Level = manager.Level; + Employees = manager.Employees.Select(context.MapFromWithContext).ToList(); + } +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } + } } \ No newline at end of file diff --git a/test/MapTo.Tests/MapTypeConverterTests.cs b/test/MapTo.Tests/MapTypeConverterTests.cs index abe0511..fe20ca5 100644 --- a/test/MapTo.Tests/MapTypeConverterTests.cs +++ b/test/MapTo.Tests/MapTypeConverterTests.cs @@ -1,283 +1,283 @@ -using System.Linq; -using MapTo.Sources; -using MapTo.Tests.Extensions; -using MapTo.Tests.Infrastructure; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Xunit; -using static MapTo.Tests.Common; - -namespace MapTo.Tests -{ - public class MapTypeConverterTests - { - [Fact] - public void VerifyMapTypeConverterAttribute() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} - -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] - public sealed class MapTypeConverterAttribute : Attribute - {{ - public MapTypeConverterAttribute(Type converter, object[] converterParameters = null) - {{ - Converter = converter; - ConverterParameters = converterParameters; - }} - - public Type Converter {{ get; }} - - public object[] ConverterParameters {{ get; }} - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); - } - - [Fact] - public void VerifyMapTypeConverterAttributeWithNullableOptionOn() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} -#nullable enable - -using System; - -namespace MapTo -{{ - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] - public sealed class MapTypeConverterAttribute : Attribute - {{ - public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null) - {{ - Converter = converter; - ConverterParameters = converterParameters; - }} - - public Type Converter {{ get; }} - - public object[]? ConverterParameters {{ get; }} - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); - } - - [Fact] - public void VerifyTypeConverterInterface() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} - -namespace MapTo -{{ - public interface ITypeConverter - {{ - TDestination Convert(TSource source, object[] converterParameters); - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); - } - - [Fact] - public void VerifyTypeConverterInterfaceWithNullableOptionOn() - { - // Arrange - const string source = ""; - var expectedInterface = $@" -{Constants.GeneratedFilesHeader} -#nullable enable - -namespace MapTo -{{ - public interface ITypeConverter - {{ - TDestination Convert(TSource source, object[]? converterParameters); - }} -}} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .WriteLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]") - .WriteLine("public string Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.WriteLine("public long Prop4 { get; set; }"))); - - source += @" -namespace Test -{ - using MapTo; - - public class Prop4Converter: ITypeConverter - { - public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string); - } -} -"; - - const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .WriteLine("[MapTypeConverter(typeof(Prop4Converter))]") - .WriteLine("public long Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }"))); - - source += @" -namespace Test -{ - using MapTo; - - public class Prop4Converter: ITypeConverter - { - public long Convert(string source, object[] converterParameters) => long.Parse(source); - } -} -"; - - const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => { builder.WriteLine("public long Prop4 { get; set; }"); }, - SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); - - var expectedResult = @" - partial class Foo - { - public Foo(Test.Models.Baz baz) - : this(new MappingContext(), baz) { } - - private protected Foo(MappingContext context, Test.Models.Baz baz) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - if (baz == null) throw new ArgumentNullException(nameof(baz)); - - context.Register(baz, this); - - Prop1 = baz.Prop1; - Prop2 = baz.Prop2; - Prop3 = baz.Prop3; - Prop4 = baz.Prop4; - } -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); - } - - [Fact] - public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError() - { - // Arrange - var source = GetSourceText(new SourceGeneratorOptions( - true, - PropertyBuilder: builder => - { - builder - .WriteLine("[IgnoreProperty]") - .WriteLine("public long IgnoreMe { get; set; }") - .WriteLine("[MapTypeConverter(typeof(Prop4Converter))]") - .WriteLine("public long Prop4 { get; set; }"); - }, - SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }"))); - - source += @" -namespace Test -{ - using MapTo; - - public class Prop4Converter: ITypeConverter - { - public int Convert(string source, object[] converterParameters) => int.Parse(source); - } -} -"; - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - var expectedError = DiagnosticsFactory.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz")); - diagnostics.ShouldNotBeSuccessful(expectedError); - } - } +using System.Linq; +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MapTypeConverterTests + { + [Fact] + public void VerifyMapTypeConverterAttribute() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} + +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class MapTypeConverterAttribute : Attribute + {{ + public MapTypeConverterAttribute(Type converter, object[] converterParameters = null) + {{ + Converter = converter; + ConverterParameters = converterParameters; + }} + + public Type Converter {{ get; }} + + public object[] ConverterParameters {{ get; }} + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); + } + + [Fact] + public void VerifyMapTypeConverterAttributeWithNullableOptionOn() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +#nullable enable + +using System; + +namespace MapTo +{{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class MapTypeConverterAttribute : Attribute + {{ + public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null) + {{ + Converter = converter; + ConverterParameters = converterParameters; + }} + + public Type Converter {{ get; }} + + public object[]? ConverterParameters {{ get; }} + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); + } + + [Fact] + public void VerifyTypeConverterInterface() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} + +namespace MapTo +{{ + public interface ITypeConverter + {{ + TDestination Convert(TSource source, object[] converterParameters); + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); + } + + [Fact] + public void VerifyTypeConverterInterfaceWithNullableOptionOn() + { + // Arrange + const string source = ""; + var expectedInterface = $@" +{Constants.GeneratedFilesHeader} +#nullable enable + +namespace MapTo +{{ + public interface ITypeConverter + {{ + TDestination Convert(TSource source, object[]? converterParameters); + }} +}} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .WriteLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]") + .WriteLine("public string Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.WriteLine("public long Prop4 { get; set; }"))); + + source += @" +namespace Test +{ + using MapTo; + + public class Prop4Converter: ITypeConverter + { + public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string); + } +} +"; + + const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .WriteLine("[MapTypeConverter(typeof(Prop4Converter))]") + .WriteLine("public long Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }"))); + + source += @" +namespace Test +{ + using MapTo; + + public class Prop4Converter: ITypeConverter + { + public long Convert(string source, object[] converterParameters) => long.Parse(source); + } +} +"; + + const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => { builder.WriteLine("public long Prop4 { get; set; }"); }, + SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); + + var expectedResult = @" + partial class Foo + { + public Foo(Test.Models.Baz baz) + : this(new MappingContext(), baz) { } + + private protected Foo(MappingContext context, Test.Models.Baz baz) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (baz == null) throw new ArgumentNullException(nameof(baz)); + + context.Register(baz, this); + + Prop1 = baz.Prop1; + Prop2 = baz.Prop2; + Prop3 = baz.Prop3; + Prop4 = baz.Prop4; + } +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); + } + + [Fact] + public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError() + { + // Arrange + var source = GetSourceText(new SourceGeneratorOptions( + true, + PropertyBuilder: builder => + { + builder + .WriteLine("[IgnoreProperty]") + .WriteLine("public long IgnoreMe { get; set; }") + .WriteLine("[MapTypeConverter(typeof(Prop4Converter))]") + .WriteLine("public long Prop4 { get; set; }"); + }, + SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }"))); + + source += @" +namespace Test +{ + using MapTo; + + public class Prop4Converter: ITypeConverter + { + public int Convert(string source, object[] converterParameters) => int.Parse(source); + } +} +"; + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + var expectedError = DiagnosticsFactory.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz")); + diagnostics.ShouldNotBeSuccessful(expectedError); + } + } } \ No newline at end of file diff --git a/test/MapTo.Tests/MappedClassesTests.cs b/test/MapTo.Tests/MappedClassesTests.cs index 009c440..5b38ef5 100644 --- a/test/MapTo.Tests/MappedClassesTests.cs +++ b/test/MapTo.Tests/MappedClassesTests.cs @@ -1,562 +1,562 @@ -using System.Collections.Generic; -using System.Linq; -using MapTo.Tests.Extensions; -using MapTo.Tests.Infrastructure; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Shouldly; -using Xunit; -using Xunit.Abstractions; -using static MapTo.Tests.Common; - -namespace MapTo.Tests -{ - public class MappedClassesTests - { - private readonly ITestOutputHelper _output; - - public MappedClassesTests(ITestOutputHelper output) - { - _output = output; - } - - [Theory] - [MemberData(nameof(SecondaryConstructorCheckData))] - public void When_SecondaryConstructorExists_Should_NotGenerateOne(string source, LanguageVersion languageVersion) - { - // Arrange - source = source.Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation - .GetGeneratedSyntaxTree("DestinationClass") - .ShouldNotBeNull() - .GetRoot() - .DescendantNodes() - .OfType() - .Count() - .ShouldBe(1); - } - - public static IEnumerable SecondaryConstructorCheckData => new List - { - new object[] - { - @" -using MapTo; -namespace Test.Data.Models -{ - public class SourceClass { public string Prop1 { get; set; } } - - [MapFrom(typeof(SourceClass))] - public partial class DestinationClass - { - public DestinationClass(SourceClass source) : this(new MappingContext(), source) { } - public string Prop1 { get; set; } - } -} -", - LanguageVersion.CSharp7_3 - }, - new object[] - { - @" -using MapTo; -namespace Test.Data.Models -{ - public record SourceClass(string Prop1); - - [MapFrom(typeof(SourceClass))] - public partial record DestinationClass(string Prop1) - { - public DestinationClass(SourceClass source) : this(new MappingContext(), source) { } - } -} -", - LanguageVersion.CSharp9 - } - }; - - [Theory] - [MemberData(nameof(SecondaryCtorWithoutPrivateCtorData))] - public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError(string source, LanguageVersion languageVersion) - { - // Arrange - source = source.Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); - - // Assert - var constructorSyntax = compilation.SyntaxTrees - .First() - .GetRoot() - .DescendantNodes() - .OfType() - .Single(); - - diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax)); - } - - public static IEnumerable SecondaryCtorWithoutPrivateCtorData => new List - { - new object[] - { - @" -using MapTo; -namespace Test.Data.Models -{ - public class SourceClass { public string Prop1 { get; set; } } - - [MapFrom(typeof(SourceClass))] - public partial class DestinationClass - { - public DestinationClass(SourceClass source) { } - public string Prop1 { get; set; } - } -} -", - LanguageVersion.CSharp7_3 - }, - new object[] - { - @" -using MapTo; -namespace Test.Data.Models -{ - public record SourceClass(string Prop1); - - [MapFrom(typeof(SourceClass))] - public partial record DestinationClass(string Prop1) - { - public DestinationClass(SourceClass source) : this(""invalid"") { } - } -} -", - LanguageVersion.CSharp9 - } - }; - - [Fact] - public void When_PropertyNameIsTheSameAsClassName_Should_MapAccordingly() - { - // Arrange - var source = @" -namespace Sale -{ - public class Sale { public Sale Prop1 { get; set; } } -} - -namespace SaleModel -{ - using MapTo; - using Sale; - - [MapFrom(typeof(Sale))] - public partial class SaleModel - { - [MapProperty(SourcePropertyName = nameof(global::Sale.Sale.Prop1))] - public Sale Sale { get; set; } - } -} -".Trim(); - - // Act - var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - } - - [Theory] - [MemberData(nameof(SameSourceAndDestinationTypeNameData))] - public void When_SourceAndDestinationNamesAreTheSame_Should_MapAccordingly(string source, LanguageVersion languageVersion) - { - // Arrange - source = source.Trim(); - - // Act - var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); - - // Assert - diagnostics.ShouldBeSuccessful(); - } - - public static IEnumerable SameSourceAndDestinationTypeNameData => new List - { - new object[] - { - @" -namespace Test -{ - public class TypeName { public int Prop2 { get; set; } } -} - -namespace Test2 -{ - using MapTo; - - [MapFrom(typeof(Test.TypeName))] - public partial class TypeName - { - [MapProperty(SourcePropertyName=""Prop2"")] - public int Prop1 { get; set; } - } -}", - LanguageVersion.CSharp7_3 - }, - new object[] - { - @" -namespace Test -{ - public record TypeName(int Prop2); -} - -namespace Test2 -{ - using MapTo; - - [MapFrom(typeof(Test.TypeName))] - public partial record TypeName([MapProperty(SourcePropertyName=""Prop2"")] int Prop1); -}", - LanguageVersion.CSharp9 - }, - new object[] - { - @" -namespace Test -{ - using System.Collections.Generic; - - public class SourceType2 { public int Id { get; set; } } - public class SourceType - { - public int Id { get; set; } - public List Prop1 { get; set; } - } -} - -namespace Test2 -{ - using MapTo; - using System.Collections.Generic; - - [MapFrom(typeof(Test.SourceType2))] - public partial class SourceType2 { public int Id { get; set; } } - - [MapFrom(typeof(Test.SourceType))] - public partial class SourceType - { - public int Id { get; set; } - public IReadOnlyList Prop1 { get; set; } - } -}", - LanguageVersion.CSharp7_3 - }, - new object[] - { - @" -namespace Test -{ - using System.Collections.Generic; - - public record SourceType(int Id, List Prop1); - public record SourceType2(int Id); -} - -namespace Test2 -{ - using MapTo; - using System.Collections.Generic; - - [MapFrom(typeof(Test.SourceType2))] - public partial record SourceType2(int Id); - - [MapFrom(typeof(Test.SourceType))] - public partial record SourceType(int Id, IReadOnlyList Prop1); -}", - LanguageVersion.CSharp9 - }, - new object[] - { - @" -namespace Test -{ - using System.Collections.Generic; - - public record SourceType1(int Id); - public record SourceType2(int Id, List Prop1); -} - -namespace Test -{ - using MapTo; - using System.Collections.Generic; - - [MapFrom(typeof(Test.SourceType1))] - public partial record SourceType3(int Id); - - [MapFrom(typeof(Test.SourceType2))] - public partial record SourceType4(int Id, IReadOnlyList Prop1); -}", - LanguageVersion.CSharp9 - } - }; - - [Theory] - [MemberData(nameof(VerifyMappedTypesData))] - public void VerifyMappedTypes(string[] sources, LanguageVersion languageVersion) - { - // Arrange - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); - - // Assert - diagnostics.ShouldBeSuccessful(); - _output.WriteLine(compilation.PrintSyntaxTree()); - } - - public static IEnumerable VerifyMappedTypesData => new List - { - new object[] { new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass }, LanguageVersion.CSharp7_3 }, - new object[] { new[] { MainSourceRecord, NestedSourceRecord, MainDestinationRecord, NestedDestinationRecord }, LanguageVersion.CSharp9 }, - new object[] - { - new[] - { - @" -namespace Test.Classes.Classes1 -{ - public class Class1 - { - public int Id { get; set; } - public string Name { get; set; } - } -}", - @" -using System; -using System.Collections.Generic; -using Test.Classes.Classes1; - -namespace Test.Classes.Classes2 -{ - public class Class2 - { - public int Id { get; set; } - public List Genres { get; set; } - public DateTime? ReleaseDate { get; set; } - } -}", - @" -using MapTo; -using System; -using System.Collections.Generic; -using TC = Test.Classes; - -namespace Tests.Records -{ - [MapFrom(typeof(Test.Classes.Classes1.Class1))] - public partial record Class1(int Id, string Name); - - [MapFrom(typeof(Test.Classes.Classes2.Class2))] - public partial record Class2(int Id, IReadOnlyList Genres); -}" - }, - LanguageVersion.CSharp9 - } - }; - - [Fact] - public void VerifySelfReferencingRecords() - { - // Arrange - var source = @" -namespace Tests.Data.Models -{ - using System.Collections.Generic; - - public record Employee(int Id, string EmployeeCode, Manager Manager); - - public record Manager(int Id, string EmployeeCode, Manager Manager, int Level, List Employees) : Employee(Id, EmployeeCode, Manager); -} - -namespace Tests.Data.ViewModels -{ - using System.Collections.Generic; - using Tests.Data.Models; - using MapTo; - - [MapFrom(typeof(Employee))] - public partial record EmployeeViewModel(int Id, string EmployeeCode, ManagerViewModel Manager); - - [MapFrom(typeof(Manager))] - public partial record ManagerViewModel(int Id, string EmployeeCode, ManagerViewModel Manager, int Level, List Employees) : EmployeeViewModel(Id, EmployeeCode, Manager); -} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9); - - // Assert - diagnostics.ShouldBeSuccessful(); - _output.WriteLine(compilation.PrintSyntaxTree()); - } - - [Fact] - public void VerifySystemNamespaceConflict() - { - // Arrange - var source = @" -namespace Test -{ - public record SomeRecord(int Id); -} - -namespace Test.Models -{ - using MapTo; - - [MapFrom(typeof(Test.SomeRecord))] - public partial record SomeRecordModel(int Id); -} - -namespace Test.System -{ - public interface IMyInterface { } -} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9); - - // Assert - diagnostics.ShouldBeSuccessful(); - _output.WriteLine(compilation.PrintSyntaxTree()); - } - - private static string MainSourceClass => @" -using System; - -namespace Test.Data.Models -{ - public class User - { - public int Id { get; set; } - - public DateTimeOffset RegisteredAt { get; set; } - - public Profile Profile { get; set; } - } -} -".Trim(); - - private static string NestedSourceClass => @" -namespace Test.Data.Models -{ - public class Profile - { - public string FirstName { get; set; } - - public string LastName { get; set; } - - public string FullName => $""{FirstName} {LastName}""; - } -} -".Trim(); - - private static string MainDestinationClass => @" -using System; -using MapTo; -using Test.Data.Models; - -namespace Test.ViewModels -{ - [MapFrom(typeof(User))] - public partial class UserViewModel - { - [MapProperty(SourcePropertyName = nameof(User.Id))] - [MapTypeConverter(typeof(IdConverter))] - public string Key { get; } - - public DateTimeOffset RegisteredAt { get; set; } - - // [IgnoreProperty] - public ProfileViewModel Profile { get; set; } - - private class IdConverter : ITypeConverter - { - public string Convert(int source, object[] converterParameters) => $""{source:X}""; - } - } -} -".Trim(); - - private static string NestedDestinationClass => @" -using MapTo; -using Test.Data.Models; - -namespace Test.ViewModels -{ - [MapFrom(typeof(Profile))] - public partial class ProfileViewModel - { - public string FirstName { get; } - - public string LastName { get; } - } -} -".Trim(); - - private static string MainSourceRecord => BuildSourceRecord("public record User(int Id, DateTimeOffset RegisteredAt, Profile Profile);"); - - private static string MainDestinationRecord => BuildDestinationRecord(@" -[MapFrom(typeof(User))] -public partial record UserViewModel( - [MapProperty(SourcePropertyName = nameof(User.Id))] - [MapTypeConverter(typeof(UserViewModel.IdConverter))] - string Key, - DateTimeOffset RegisteredAt, - Profile Profile) -{ - private class IdConverter : ITypeConverter - { - public string Convert(int source, object[] converterParameters) => $""{source:X}""; - } -}"); - - private static string NestedSourceRecord => BuildSourceRecord("public record Profile(string FirstName, string LastName) { public string FullName => $\"{FirstName} {LastName}\"; }"); - - private static string NestedDestinationRecord => BuildDestinationRecord("[MapFrom(typeof(Profile))] public partial record ProfileViewModel(string FirstName, string LastName);"); - - private static string BuildSourceRecord(string record) - { - return $@" -using System; - -namespace RecordTest.Data.Models -{{ - {record} -}} -".Trim(); - } - - private static string BuildDestinationRecord(string record) - { - return $@" -using System; -using MapTo; -using RecordTest.Data.Models; - -namespace RecordTest.ViewModels -{{ - {record} -}} -".Trim(); - } - } +using System.Collections.Generic; +using System.Linq; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Shouldly; +using Xunit; +using Xunit.Abstractions; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MappedClassesTests + { + private readonly ITestOutputHelper _output; + + public MappedClassesTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [MemberData(nameof(SecondaryConstructorCheckData))] + public void When_SecondaryConstructorExists_Should_NotGenerateOne(string source, LanguageVersion languageVersion) + { + // Arrange + source = source.Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation + .GetGeneratedSyntaxTree("DestinationClass") + .ShouldNotBeNull() + .GetRoot() + .DescendantNodes() + .OfType() + .Count() + .ShouldBe(1); + } + + public static IEnumerable SecondaryConstructorCheckData => new List + { + new object[] + { + @" +using MapTo; +namespace Test.Data.Models +{ + public class SourceClass { public string Prop1 { get; set; } } + + [MapFrom(typeof(SourceClass))] + public partial class DestinationClass + { + public DestinationClass(SourceClass source) : this(new MappingContext(), source) { } + public string Prop1 { get; set; } + } +} +", + LanguageVersion.CSharp7_3 + }, + new object[] + { + @" +using MapTo; +namespace Test.Data.Models +{ + public record SourceClass(string Prop1); + + [MapFrom(typeof(SourceClass))] + public partial record DestinationClass(string Prop1) + { + public DestinationClass(SourceClass source) : this(new MappingContext(), source) { } + } +} +", + LanguageVersion.CSharp9 + } + }; + + [Theory] + [MemberData(nameof(SecondaryCtorWithoutPrivateCtorData))] + public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError(string source, LanguageVersion languageVersion) + { + // Arrange + source = source.Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); + + // Assert + var constructorSyntax = compilation.SyntaxTrees + .First() + .GetRoot() + .DescendantNodes() + .OfType() + .Single(); + + diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax)); + } + + public static IEnumerable SecondaryCtorWithoutPrivateCtorData => new List + { + new object[] + { + @" +using MapTo; +namespace Test.Data.Models +{ + public class SourceClass { public string Prop1 { get; set; } } + + [MapFrom(typeof(SourceClass))] + public partial class DestinationClass + { + public DestinationClass(SourceClass source) { } + public string Prop1 { get; set; } + } +} +", + LanguageVersion.CSharp7_3 + }, + new object[] + { + @" +using MapTo; +namespace Test.Data.Models +{ + public record SourceClass(string Prop1); + + [MapFrom(typeof(SourceClass))] + public partial record DestinationClass(string Prop1) + { + public DestinationClass(SourceClass source) : this(""invalid"") { } + } +} +", + LanguageVersion.CSharp9 + } + }; + + [Fact] + public void When_PropertyNameIsTheSameAsClassName_Should_MapAccordingly() + { + // Arrange + var source = @" +namespace Sale +{ + public class Sale { public Sale Prop1 { get; set; } } +} + +namespace SaleModel +{ + using MapTo; + using Sale; + + [MapFrom(typeof(Sale))] + public partial class SaleModel + { + [MapProperty(SourcePropertyName = nameof(global::Sale.Sale.Prop1))] + public Sale Sale { get; set; } + } +} +".Trim(); + + // Act + var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + } + + [Theory] + [MemberData(nameof(SameSourceAndDestinationTypeNameData))] + public void When_SourceAndDestinationNamesAreTheSame_Should_MapAccordingly(string source, LanguageVersion languageVersion) + { + // Arrange + source = source.Trim(); + + // Act + var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); + + // Assert + diagnostics.ShouldBeSuccessful(); + } + + public static IEnumerable SameSourceAndDestinationTypeNameData => new List + { + new object[] + { + @" +namespace Test +{ + public class TypeName { public int Prop2 { get; set; } } +} + +namespace Test2 +{ + using MapTo; + + [MapFrom(typeof(Test.TypeName))] + public partial class TypeName + { + [MapProperty(SourcePropertyName=""Prop2"")] + public int Prop1 { get; set; } + } +}", + LanguageVersion.CSharp7_3 + }, + new object[] + { + @" +namespace Test +{ + public record TypeName(int Prop2); +} + +namespace Test2 +{ + using MapTo; + + [MapFrom(typeof(Test.TypeName))] + public partial record TypeName([MapProperty(SourcePropertyName=""Prop2"")] int Prop1); +}", + LanguageVersion.CSharp9 + }, + new object[] + { + @" +namespace Test +{ + using System.Collections.Generic; + + public class SourceType2 { public int Id { get; set; } } + public class SourceType + { + public int Id { get; set; } + public List Prop1 { get; set; } + } +} + +namespace Test2 +{ + using MapTo; + using System.Collections.Generic; + + [MapFrom(typeof(Test.SourceType2))] + public partial class SourceType2 { public int Id { get; set; } } + + [MapFrom(typeof(Test.SourceType))] + public partial class SourceType + { + public int Id { get; set; } + public IReadOnlyList Prop1 { get; set; } + } +}", + LanguageVersion.CSharp7_3 + }, + new object[] + { + @" +namespace Test +{ + using System.Collections.Generic; + + public record SourceType(int Id, List Prop1); + public record SourceType2(int Id); +} + +namespace Test2 +{ + using MapTo; + using System.Collections.Generic; + + [MapFrom(typeof(Test.SourceType2))] + public partial record SourceType2(int Id); + + [MapFrom(typeof(Test.SourceType))] + public partial record SourceType(int Id, IReadOnlyList Prop1); +}", + LanguageVersion.CSharp9 + }, + new object[] + { + @" +namespace Test +{ + using System.Collections.Generic; + + public record SourceType1(int Id); + public record SourceType2(int Id, List Prop1); +} + +namespace Test +{ + using MapTo; + using System.Collections.Generic; + + [MapFrom(typeof(Test.SourceType1))] + public partial record SourceType3(int Id); + + [MapFrom(typeof(Test.SourceType2))] + public partial record SourceType4(int Id, IReadOnlyList Prop1); +}", + LanguageVersion.CSharp9 + } + }; + + [Theory] + [MemberData(nameof(VerifyMappedTypesData))] + public void VerifyMappedTypes(string[] sources, LanguageVersion languageVersion) + { + // Arrange + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); + + // Assert + diagnostics.ShouldBeSuccessful(); + _output.WriteLine(compilation.PrintSyntaxTree()); + } + + public static IEnumerable VerifyMappedTypesData => new List + { + new object[] { new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass }, LanguageVersion.CSharp7_3 }, + new object[] { new[] { MainSourceRecord, NestedSourceRecord, MainDestinationRecord, NestedDestinationRecord }, LanguageVersion.CSharp9 }, + new object[] + { + new[] + { + @" +namespace Test.Classes.Classes1 +{ + public class Class1 + { + public int Id { get; set; } + public string Name { get; set; } + } +}", + @" +using System; +using System.Collections.Generic; +using Test.Classes.Classes1; + +namespace Test.Classes.Classes2 +{ + public class Class2 + { + public int Id { get; set; } + public List Genres { get; set; } + public DateTime? ReleaseDate { get; set; } + } +}", + @" +using MapTo; +using System; +using System.Collections.Generic; +using TC = Test.Classes; + +namespace Tests.Records +{ + [MapFrom(typeof(Test.Classes.Classes1.Class1))] + public partial record Class1(int Id, string Name); + + [MapFrom(typeof(Test.Classes.Classes2.Class2))] + public partial record Class2(int Id, IReadOnlyList Genres); +}" + }, + LanguageVersion.CSharp9 + } + }; + + [Fact] + public void VerifySelfReferencingRecords() + { + // Arrange + var source = @" +namespace Tests.Data.Models +{ + using System.Collections.Generic; + + public record Employee(int Id, string EmployeeCode, Manager Manager); + + public record Manager(int Id, string EmployeeCode, Manager Manager, int Level, List Employees) : Employee(Id, EmployeeCode, Manager); +} + +namespace Tests.Data.ViewModels +{ + using System.Collections.Generic; + using Tests.Data.Models; + using MapTo; + + [MapFrom(typeof(Employee))] + public partial record EmployeeViewModel(int Id, string EmployeeCode, ManagerViewModel Manager); + + [MapFrom(typeof(Manager))] + public partial record ManagerViewModel(int Id, string EmployeeCode, ManagerViewModel Manager, int Level, List Employees) : EmployeeViewModel(Id, EmployeeCode, Manager); +} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9); + + // Assert + diagnostics.ShouldBeSuccessful(); + _output.WriteLine(compilation.PrintSyntaxTree()); + } + + [Fact] + public void VerifySystemNamespaceConflict() + { + // Arrange + var source = @" +namespace Test +{ + public record SomeRecord(int Id); +} + +namespace Test.Models +{ + using MapTo; + + [MapFrom(typeof(Test.SomeRecord))] + public partial record SomeRecordModel(int Id); +} + +namespace Test.System +{ + public interface IMyInterface { } +} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9); + + // Assert + diagnostics.ShouldBeSuccessful(); + _output.WriteLine(compilation.PrintSyntaxTree()); + } + + private static string MainSourceClass => @" +using System; + +namespace Test.Data.Models +{ + public class User + { + public int Id { get; set; } + + public DateTimeOffset RegisteredAt { get; set; } + + public Profile Profile { get; set; } + } +} +".Trim(); + + private static string NestedSourceClass => @" +namespace Test.Data.Models +{ + public class Profile + { + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string FullName => $""{FirstName} {LastName}""; + } +} +".Trim(); + + private static string MainDestinationClass => @" +using System; +using MapTo; +using Test.Data.Models; + +namespace Test.ViewModels +{ + [MapFrom(typeof(User))] + public partial class UserViewModel + { + [MapProperty(SourcePropertyName = nameof(User.Id))] + [MapTypeConverter(typeof(IdConverter))] + public string Key { get; } + + public DateTimeOffset RegisteredAt { get; set; } + + // [IgnoreProperty] + public ProfileViewModel Profile { get; set; } + + private class IdConverter : ITypeConverter + { + public string Convert(int source, object[] converterParameters) => $""{source:X}""; + } + } +} +".Trim(); + + private static string NestedDestinationClass => @" +using MapTo; +using Test.Data.Models; + +namespace Test.ViewModels +{ + [MapFrom(typeof(Profile))] + public partial class ProfileViewModel + { + public string FirstName { get; } + + public string LastName { get; } + } +} +".Trim(); + + private static string MainSourceRecord => BuildSourceRecord("public record User(int Id, DateTimeOffset RegisteredAt, Profile Profile);"); + + private static string MainDestinationRecord => BuildDestinationRecord(@" +[MapFrom(typeof(User))] +public partial record UserViewModel( + [MapProperty(SourcePropertyName = nameof(User.Id))] + [MapTypeConverter(typeof(UserViewModel.IdConverter))] + string Key, + DateTimeOffset RegisteredAt, + Profile Profile) +{ + private class IdConverter : ITypeConverter + { + public string Convert(int source, object[] converterParameters) => $""{source:X}""; + } +}"); + + private static string NestedSourceRecord => BuildSourceRecord("public record Profile(string FirstName, string LastName) { public string FullName => $\"{FirstName} {LastName}\"; }"); + + private static string NestedDestinationRecord => BuildDestinationRecord("[MapFrom(typeof(Profile))] public partial record ProfileViewModel(string FirstName, string LastName);"); + + private static string BuildSourceRecord(string record) + { + return $@" +using System; + +namespace RecordTest.Data.Models +{{ + {record} +}} +".Trim(); + } + + private static string BuildDestinationRecord(string record) + { + return $@" +using System; +using MapTo; +using RecordTest.Data.Models; + +namespace RecordTest.ViewModels +{{ + {record} +}} +".Trim(); + } + } } \ No newline at end of file diff --git a/test/MapTo.Tests/MappingContextTests.cs b/test/MapTo.Tests/MappingContextTests.cs index 410a0c5..70981fa 100644 --- a/test/MapTo.Tests/MappingContextTests.cs +++ b/test/MapTo.Tests/MappingContextTests.cs @@ -1,102 +1,102 @@ -using MapTo.Sources; -using MapTo.Tests.Extensions; -using MapTo.Tests.Infrastructure; -using Xunit; -using static MapTo.Tests.Common; - -namespace MapTo.Tests -{ - public class MappingContextTests - { - [Fact] - public void VerifyMappingContextSource() - { - // Arrange - const string source = ""; - var expected = @" -// - -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace MapTo -{ - internal sealed class MappingContext - { - private readonly Dictionary _cache; - - internal MappingContext() - { - _cache = new Dictionary(1); - } - - internal static TMapped Create(TOriginal original) - { - if (original == null) throw new ArgumentNullException(nameof(original)); - - var context = new MappingContext(); - var mapped = context.MapFromWithContext(original); - - if (mapped == null) - { - throw new InvalidOperationException(); - } - - return mapped; - } - - internal TMapped MapFromWithContext(TOriginal original) - { - if (original == null) - { - return default(TMapped); - } - - if (!TryGetValue(original, out var mapped)) - { - var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null); - if (instance != null) - { - mapped = (TMapped)instance; - } - } - - return mapped; - } - - internal void Register(TOriginal original, TMapped mapped) - { - if (original == null) throw new ArgumentNullException(nameof(original)); - if (mapped == null) throw new ArgumentNullException(nameof(mapped)); - - if (!_cache.ContainsKey(original)) - { - _cache.Add(original, mapped); - } - } - - private bool TryGetValue(TOriginal original, out TMapped mapped) - { - if (original != null && _cache.TryGetValue(original, out var value)) - { - mapped = (TMapped)value; - return true; - } - - mapped = default(TMapped); - return false; - } - } -} -".Trim(); - - // Act - var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); - - // Assert - diagnostics.ShouldBeSuccessful(); - compilation.SyntaxTrees.ShouldContainSource(MappingContextSource.ClassName, expected); - } - } +using MapTo.Sources; +using MapTo.Tests.Extensions; +using MapTo.Tests.Infrastructure; +using Xunit; +using static MapTo.Tests.Common; + +namespace MapTo.Tests +{ + public class MappingContextTests + { + [Fact] + public void VerifyMappingContextSource() + { + // Arrange + const string source = ""; + var expected = @" +// + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MapTo +{ + internal sealed class MappingContext + { + private readonly Dictionary _cache; + + internal MappingContext() + { + _cache = new Dictionary(1); + } + + internal static TMapped Create(TOriginal original) + { + if (original == null) throw new ArgumentNullException(nameof(original)); + + var context = new MappingContext(); + var mapped = context.MapFromWithContext(original); + + if (mapped == null) + { + throw new InvalidOperationException(); + } + + return mapped; + } + + internal TMapped MapFromWithContext(TOriginal original) + { + if (original == null) + { + return default(TMapped); + } + + if (!TryGetValue(original, out var mapped)) + { + var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null); + if (instance != null) + { + mapped = (TMapped)instance; + } + } + + return mapped; + } + + internal void Register(TOriginal original, TMapped mapped) + { + if (original == null) throw new ArgumentNullException(nameof(original)); + if (mapped == null) throw new ArgumentNullException(nameof(mapped)); + + if (!_cache.ContainsKey(original)) + { + _cache.Add(original, mapped); + } + } + + private bool TryGetValue(TOriginal original, out TMapped mapped) + { + if (original != null && _cache.TryGetValue(original, out var value)) + { + mapped = (TMapped)value; + return true; + } + + mapped = default(TMapped); + return false; + } + } +} +".Trim(); + + // Act + var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); + + // Assert + diagnostics.ShouldBeSuccessful(); + compilation.SyntaxTrees.ShouldContainSource(MappingContextSource.ClassName, expected); + } + } } \ No newline at end of file diff --git a/test/TestConsoleApp/Data/FinanceTransaction.cs b/test/TestConsoleApp/Data/FinanceTransaction.cs index 90626b0..8105767 100644 --- a/test/TestConsoleApp/Data/FinanceTransaction.cs +++ b/test/TestConsoleApp/Data/FinanceTransaction.cs @@ -1,58 +1,59 @@ -using System; -using MapTo; - -namespace BlueWest.Data -{ - public enum FinanceSymbol - { - BTC_EUR, - BTC_BUSD, - BTC_USD, - BTC_USDT, - LTC_EUR, - LTC_BUSD, - LTC_USDT - - - - - - - - - - } - - public enum FinanceTransactionType - { - Buy, - Sell - } - - [MapFrom(typeof(FinanceTransactionInsertDto))] - public partial struct FinanceTransaction - { - public readonly int Id; - public readonly int UserId; - public readonly FinanceTransactionType FinanceTransactionType; - public readonly FinanceSymbol FinanceSymbol; - public readonly double Amount; // To Buy - public readonly double Quantity; // Bought - public readonly double Fee; - public readonly DateTime DateTime; - - - public FinanceTransaction(int id, int userId, FinanceTransactionType financeTransactionType, - FinanceSymbol financeSymbol, double amount, double quantity, double fee, DateTime dateTime) - { - Id = id; - UserId = userId; - FinanceTransactionType = financeTransactionType; - FinanceSymbol = financeSymbol; - Amount = amount; - Quantity = quantity; - Fee = fee; - DateTime = dateTime; - } - } +using System; +using MapTo; + +namespace BlueWest.Data +{ + public enum FinanceSymbol + { + BTC_EUR, + BTC_BUSD, + BTC_USD, + BTC_USDT, + LTC_EUR, + LTC_BUSD, + LTC_USDT + + + + + + + + + + } + + public enum FinanceTransactionType + { + Buy, + Sell + } + + [JsonExtension] + [MapFrom(typeof(FinanceTransactionInsertDto))] + public partial struct FinanceTransaction + { + public readonly int Id; + public readonly int UserId; + public readonly FinanceTransactionType FinanceTransactionType; + public readonly FinanceSymbol FinanceSymbol; + public readonly double Amount; // To Buy + public readonly double Quantity; // Bought + public readonly double Fee; + public readonly DateTime DateTime; + + + public FinanceTransaction(int id, int userId, FinanceTransactionType financeTransactionType, + FinanceSymbol financeSymbol, double amount, double quantity, double fee, DateTime dateTime) + { + Id = id; + UserId = userId; + FinanceTransactionType = financeTransactionType; + FinanceSymbol = financeSymbol; + Amount = amount; + Quantity = quantity; + Fee = fee; + DateTime = dateTime; + } + } } \ No newline at end of file diff --git a/test/TestConsoleApp/Data/FinanceTransactionInsertDto.cs b/test/TestConsoleApp/Data/FinanceTransactionInsertDto.cs index d91b03a..ea81b87 100644 --- a/test/TestConsoleApp/Data/FinanceTransactionInsertDto.cs +++ b/test/TestConsoleApp/Data/FinanceTransactionInsertDto.cs @@ -1,16 +1,16 @@ -using System; - -namespace BlueWest.Data -{ - - public partial struct FinanceTransactionInsertDto - { - public int UserId { get; set; } - public FinanceTransactionType FinanceTransactionType { get; } - public FinanceSymbol FinanceSymbol { get; } - public double Amount { get; } // To Buy - public double Quantity { get; } // Bought - public double Fee { get; } - public DateTime DateTime { get; } - } +using System; + +namespace BlueWest.Data +{ + + public partial struct FinanceTransactionInsertDto + { + public readonly int UserId; + public readonly FinanceTransactionType FinanceTransactionType; + public readonly FinanceSymbol FinanceSymbol; + public readonly double Amount; // To Buy + public readonly double Quantity; // Bought + public readonly double Fee; + public readonly DateTime DateTime; + } } \ No newline at end of file diff --git a/test/TestConsoleApp/Data/FinanceTransactionReadDto.cs b/test/TestConsoleApp/Data/FinanceTransactionReadDto.cs index bfb5a24..30ba600 100644 --- a/test/TestConsoleApp/Data/FinanceTransactionReadDto.cs +++ b/test/TestConsoleApp/Data/FinanceTransactionReadDto.cs @@ -1,22 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Text; -using MapTo; - -namespace BlueWest.Data -{ - [MapFrom(typeof(FinanceTransaction))] - - partial struct FinanceTransactionReadDto - { - public int UserId { get; set; } - public FinanceTransactionType FinanceTransactionType { get; } - public FinanceSymbol FinanceSymbol { get; } - public double Amount { get; } // To Buy - public double Quantity { get; } // Bought - public double Fee { get; } - public DateTime DateTime { get; } - - public string ReadData { get; } - } -} +using System; +using System.Collections.Generic; +using System.Text; +using MapTo; + +namespace BlueWest.Data +{ + [MapFrom(typeof(FinanceTransaction))] + + partial struct FinanceTransactionReadDto + { + public readonly int UserId; + public readonly FinanceTransactionType FinanceTransactionType; + public readonly FinanceSymbol FinanceSymbol; + public readonly double Amount; // To Buy + public readonly double Quantity; // Bought + public readonly double Fee; + public readonly DateTime DateTime; + + public readonly string ReadData; + } +} diff --git a/test/TestConsoleApp/Data/Models/Car.cs b/test/TestConsoleApp/Data/Models/Car.cs index fe6f130..4b812c6 100644 --- a/test/TestConsoleApp/Data/Models/Car.cs +++ b/test/TestConsoleApp/Data/Models/Car.cs @@ -1,27 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MapTo; -using TestConsoleApp.ViewModels; - -namespace TestConsoleApp.Data.Models -{ - [MapFrom(typeof(CarReadDto))] - [UseUpdate] - partial class Car - { - public int Size { get; } - public int Id { get; } - - public string Brand { get; } - - public Car(int size, int id, string brand) - { - Size = size; - Id = id; - Brand = brand; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MapTo; +using TestConsoleApp.ViewModels; + +namespace TestConsoleApp.Data.Models +{ + [MapFrom(typeof(CarReadDto))] + [UseUpdate] + partial class Car + { + public int Size { get; } + public int Id { get; } + + public string Brand { get; } + + public Car(int size, int id, string brand) + { + Size = size; + Id = id; + Brand = brand; + } + } +} diff --git a/test/TestConsoleApp/Data/Models/Employee.cs b/test/TestConsoleApp/Data/Models/Employee.cs index a25a6d9..e7c7118 100644 --- a/test/TestConsoleApp/Data/Models/Employee.cs +++ b/test/TestConsoleApp/Data/Models/Employee.cs @@ -1,21 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace TestConsoleApp.Data.Models -{ - - public class Employee - { - public int Id { get; } - - public string EmployeeCode { get; } - - public Employee(int id, string employeeCode) - { - Id = id; - EmployeeCode = employeeCode; - } - - } -} +using System; +using System.Collections.Generic; +using System.Text; + +namespace TestConsoleApp.Data.Models +{ + + public class Employee + { + public int Id { get; } + + public string EmployeeCode { get; } + + public Employee(int id, string employeeCode) + { + Id = id; + EmployeeCode = employeeCode; + } + + } +} diff --git a/test/TestConsoleApp/Data/Models/MyStruct.cs b/test/TestConsoleApp/Data/Models/MyStruct.cs index 2be6f38..8d09d44 100644 --- a/test/TestConsoleApp/Data/Models/MyStruct.cs +++ b/test/TestConsoleApp/Data/Models/MyStruct.cs @@ -1,26 +1,26 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TestConsoleApp.ViewModels; -using MapTo; - -namespace TestConsoleApp.Data.Models -{ - [MapFrom(typeof(MyStructViewModel))] - [UseUpdate] - public partial struct MyStruct - { - public int SomeInt { get; set; } - - public string ReadOnlyString { get; } - - public MyStruct(int someInt, string readOnlyString) - { - SomeInt = someInt; - ReadOnlyString = readOnlyString; - } - - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TestConsoleApp.ViewModels; +using MapTo; + +namespace TestConsoleApp.Data.Models +{ + [MapFrom(typeof(MyStructViewModel))] + [UseUpdate] + public partial struct MyStruct + { + public int SomeInt { get; set; } + + public string ReadOnlyString { get; } + + public MyStruct(int someInt, string readOnlyString) + { + SomeInt = someInt; + ReadOnlyString = readOnlyString; + } + + } +} diff --git a/test/TestConsoleApp/Data/User.cs b/test/TestConsoleApp/Data/User.cs index b92a5f7..ed36a9a 100644 --- a/test/TestConsoleApp/Data/User.cs +++ b/test/TestConsoleApp/Data/User.cs @@ -1,43 +1,44 @@ - -using System.Collections.Generic; -using MapTo; - - -namespace BlueWest.Data -{ - [MapFrom(typeof(UserUpdateDto))] - public partial class User - { - public readonly int Id; - public string Name; - public string Address; - - public string BTCAddress; - public string LTCAddress; - - public double BTCAmount; - public double LTCAmount; - - public readonly List FinanceTransactions; - - public User(int id, string name, string address, string btcAddress, string ltcAddress, double btcAmount, double ltcAmount, List financeTransactions) - { - Id = id; - Name = name; - Address = address; - BTCAddress = btcAddress; - LTCAddress = ltcAddress; - BTCAmount = btcAmount; - LTCAmount = ltcAmount; - FinanceTransactions = financeTransactions; - } - - public void AddTransaction(FinanceTransaction financeTransaction) - { - FinanceTransactions.Add(financeTransaction); - } - - } -} - - + +using System.Collections.Generic; +using MapTo; + + +namespace BlueWest.Data +{ + [MapFrom(typeof(UserUpdateDto))] + [JsonExtension] + public partial class User + { + public readonly int Id; + public string Name; + public string Address; + + public string BTCAddress; + public string LTCAddress; + + public double BTCAmount; + public double LTCAmount; + + public readonly List FinanceTransactions; + + public User(int id, string name, string address, string btcAddress, string ltcAddress, double btcAmount, double ltcAmount, List financeTransactions) + { + Id = id; + Name = name; + Address = address; + BTCAddress = btcAddress; + LTCAddress = ltcAddress; + BTCAmount = btcAmount; + LTCAmount = ltcAmount; + FinanceTransactions = financeTransactions; + } + + public void AddTransaction(FinanceTransaction financeTransaction) + { + FinanceTransactions.Add(financeTransaction); + } + + } +} + + diff --git a/test/TestConsoleApp/Data/UserList.cs b/test/TestConsoleApp/Data/UserList.cs index b653d18..78de267 100644 --- a/test/TestConsoleApp/Data/UserList.cs +++ b/test/TestConsoleApp/Data/UserList.cs @@ -1,16 +1,16 @@ -using System.Collections.Generic; - -namespace BlueWest.Data -{ - public class UserList - { - public List Users; - - public UserList(List users) - { - Users = users; - } - - public int Length => Users.Count; - } +using System.Collections.Generic; + +namespace BlueWest.Data +{ + public class UserList + { + public List Users; + + public UserList(List users) + { + Users = users; + } + + public int Length => Users.Count; + } } \ No newline at end of file diff --git a/test/TestConsoleApp/Data/UserUpdateDto.cs b/test/TestConsoleApp/Data/UserUpdateDto.cs index a1b18d3..957beb9 100644 --- a/test/TestConsoleApp/Data/UserUpdateDto.cs +++ b/test/TestConsoleApp/Data/UserUpdateDto.cs @@ -1,19 +1,19 @@ -using MapTo; - -namespace BlueWest.Data -{ - [MapFrom(typeof(User))] - - public partial class UserUpdateDto - { - public string Name; - public string Address; - - public string BTCAddress; - public string LTCAddress; - - public double BTCAmount; - public double LTCAmount; - - } +using MapTo; + +namespace BlueWest.Data +{ + [MapFrom(typeof(User))] + + public partial class UserUpdateDto + { + public string Name; + public string Address; + + public string BTCAddress; + public string LTCAddress; + + public double BTCAmount; + public double LTCAmount; + + } } \ No newline at end of file diff --git a/test/TestConsoleApp/Program.cs b/test/TestConsoleApp/Program.cs index 06a1bc2..43517ac 100644 --- a/test/TestConsoleApp/Program.cs +++ b/test/TestConsoleApp/Program.cs @@ -1,32 +1,32 @@ -using System; -using MapTo; -using TestConsoleApp.Data.Models; -using TestConsoleApp.ViewModels; - -namespace TestConsoleApp -{ - internal class Program - { - private static void Main(string[] args) - { - //UserTest(); - - // EmployeeManagerTest(); - Console.WriteLine("done"); - } - - private static void EmployeeManagerTest() - { - - - var employee = new Employee(1, "hello"); - - - - - } - - - - } +using System; +using MapTo; +using TestConsoleApp.Data.Models; +using TestConsoleApp.ViewModels; + +namespace TestConsoleApp +{ + internal class Program + { + private static void Main(string[] args) + { + //UserTest(); + + // EmployeeManagerTest(); + Console.WriteLine("done"); + } + + private static void EmployeeManagerTest() + { + + + var employee = new Employee(1, "hello"); + + + + + } + + + + } } \ No newline at end of file diff --git a/test/TestConsoleApp/TestConsoleApp.csproj b/test/TestConsoleApp/TestConsoleApp.csproj index 989f8e5..2ab23e7 100644 --- a/test/TestConsoleApp/TestConsoleApp.csproj +++ b/test/TestConsoleApp/TestConsoleApp.csproj @@ -1,18 +1,18 @@ - - - - Exe - net471 - latest - enable - - - - - - - - - Internal - - + + + + Exe + net471 + latest + enable + + + + + + + + + Internal + + diff --git a/test/TestConsoleApp/ViewModels/CarReadDto.cs b/test/TestConsoleApp/ViewModels/CarReadDto.cs index 2241eff..75df73c 100644 --- a/test/TestConsoleApp/ViewModels/CarReadDto.cs +++ b/test/TestConsoleApp/ViewModels/CarReadDto.cs @@ -1,23 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MapTo; -using TestConsoleApp.Data.Models; - -namespace TestConsoleApp.ViewModels -{ - [MapFrom(typeof(Car))] - partial class CarReadDto - { - public int Size { get; } - public string Brand { get; } - - public CarReadDto(int size, string brand) - { - Size = size; - Brand = brand; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MapTo; +using TestConsoleApp.Data.Models; + +namespace TestConsoleApp.ViewModels +{ + [MapFrom(typeof(Car))] + partial class CarReadDto + { + public int Size { get; } + public string Brand { get; } + + public CarReadDto(int size, string brand) + { + Size = size; + Brand = brand; + } + } +} diff --git a/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs b/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs index 6c3adf4..8750e6c 100644 --- a/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs +++ b/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs @@ -1,13 +1,13 @@ -using MapTo; -using TestConsoleApp.Data.Models; - -namespace TestConsoleApp.ViewModels -{ - [MapFrom(typeof(Employee))] - public partial class EmployeeViewModel - { - public int Id { get; } - - - } -} +using MapTo; +using TestConsoleApp.Data.Models; + +namespace TestConsoleApp.ViewModels +{ + [MapFrom(typeof(Employee))] + public partial class EmployeeViewModel + { + public int Id { get; } + + + } +} diff --git a/test/TestConsoleApp/ViewModels/MyStructViewModel.cs b/test/TestConsoleApp/ViewModels/MyStructViewModel.cs index 46577ea..b542044 100644 --- a/test/TestConsoleApp/ViewModels/MyStructViewModel.cs +++ b/test/TestConsoleApp/ViewModels/MyStructViewModel.cs @@ -1,22 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TestConsoleApp.Data.Models; -using MapTo; - -namespace TestConsoleApp.ViewModels -{ - [MapFrom(typeof(MyStruct))] - - public partial struct MyStructViewModel - { - public int SomeInt { get; set; } - - public MyStructViewModel(int someInt) - { - SomeInt = someInt; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TestConsoleApp.Data.Models; +using MapTo; + +namespace TestConsoleApp.ViewModels +{ + [MapFrom(typeof(MyStruct))] + + public partial struct MyStructViewModel + { + public int SomeInt { get; set; } + + public MyStructViewModel(int someInt) + { + SomeInt = someInt; + } + } +}