Compare commits

..

1 Commits

Author SHA1 Message Date
Wvader fd294925fc struct 2021-12-06 02:06:54 +00:00
91 changed files with 5663 additions and 6734 deletions

View File

@ -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}] [*.{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_style = space
indent_size = 4 indent_size = 4
tab_width = 4 tab_width = 4
[*] [*]
# Microsoft .NET properties # Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers = false 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_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_space_after_cast = false
csharp_style_var_elsewhere = true:suggestion csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = 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_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_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_parentheses_in_relational_binary_operators = never_if_unnecessary:none
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_qualification_for_event = false:suggestion dotnet_style_qualification_for_event = false:suggestion
dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_property = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# ReSharper properties # ReSharper properties
resharper_autodetect_indent_settings = true resharper_autodetect_indent_settings = true
resharper_blank_lines_after_control_transfer_statements = 1 resharper_blank_lines_after_control_transfer_statements = 1
resharper_blank_lines_after_multiline_statements = 1 resharper_blank_lines_after_multiline_statements = 1
resharper_blank_lines_around_block_case_section = 1 resharper_blank_lines_around_block_case_section = 1
resharper_blank_lines_around_multiline_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_auto_property = 1
resharper_blank_lines_around_single_line_local_method = 1 resharper_blank_lines_around_single_line_local_method = 1
resharper_blank_lines_around_single_line_property = 1 resharper_blank_lines_around_single_line_property = 1
resharper_braces_for_for = required resharper_braces_for_for = required
resharper_braces_for_foreach = required resharper_braces_for_foreach = required
resharper_braces_for_ifelse = required resharper_braces_for_ifelse = required
resharper_braces_for_while = required resharper_braces_for_while = required
resharper_csharp_blank_lines_around_single_line_invocable = 1 resharper_csharp_blank_lines_around_single_line_invocable = 1
resharper_csharp_empty_block_style = together_same_line resharper_csharp_empty_block_style = together_same_line
resharper_csharp_keep_blank_lines_in_code = 1 resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1 resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = 180 resharper_csharp_max_line_length = 180
resharper_csharp_wrap_lines = false resharper_csharp_wrap_lines = false
resharper_local_function_body = expression_body resharper_local_function_body = expression_body
resharper_method_or_operator_body = expression_body resharper_method_or_operator_body = expression_body
resharper_place_accessorholder_attribute_on_same_line = false resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false resharper_place_field_attribute_on_same_line = false
resharper_space_after_cast = false resharper_space_after_cast = false
resharper_space_within_single_line_array_initializer_braces = true resharper_space_within_single_line_array_initializer_braces = true
resharper_use_indent_from_vs = false resharper_use_indent_from_vs = false
resharper_xmldoc_indent_text = ZeroIndent resharper_xmldoc_indent_text = ZeroIndent
# ReSharper inspection severities # ReSharper inspection severities
resharper_arguments_style_literal_highlighting = none resharper_arguments_style_literal_highlighting = none
resharper_arguments_style_named_expression_highlighting = none resharper_arguments_style_named_expression_highlighting = none
resharper_arguments_style_other_highlighting = none resharper_arguments_style_other_highlighting = none
resharper_arrange_redundant_parentheses_highlighting = hint resharper_arrange_redundant_parentheses_highlighting = hint
resharper_arrange_this_qualifier_highlighting = hint resharper_arrange_this_qualifier_highlighting = hint
resharper_arrange_type_member_modifiers_highlighting = hint resharper_arrange_type_member_modifiers_highlighting = hint
resharper_arrange_type_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_for_member_access_highlighting = hint
resharper_built_in_type_reference_style_highlighting = hint resharper_built_in_type_reference_style_highlighting = hint
resharper_class_never_instantiated_global_highlighting = none resharper_class_never_instantiated_global_highlighting = none
resharper_redundant_base_qualifier_highlighting = warning resharper_redundant_base_qualifier_highlighting = warning
resharper_suggest_var_or_type_built_in_types_highlighting = hint resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint resharper_suggest_var_or_type_simple_types_highlighting = hint
resharper_web_config_module_not_resolved_highlighting = warning resharper_web_config_module_not_resolved_highlighting = warning
resharper_web_config_type_not_resolved_highlighting = warning resharper_web_config_type_not_resolved_highlighting = warning
resharper_web_config_wrong_module_highlighting = warning resharper_web_config_wrong_module_highlighting = warning

View File

@ -1,28 +1,28 @@
name: Build and Test name: Build and Test
on: on:
pull_request: pull_request:
branches: [ master ] branches: [ master ]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work. fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- uses: dotnet/nbgv@v0.4.0 - uses: dotnet/nbgv@v0.4.0
with: with:
setAllVars: true setAllVars: true
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 5.0.101 dotnet-version: 5.0.101
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
run: dotnet build --no-restore run: dotnet build --no-restore
- name: Test - name: Test
run: dotnet test --no-build --verbosity normal run: dotnet test --no-build --verbosity normal

View File

@ -1,37 +1,37 @@
name: Publish Packages name: Publish Packages
on: on:
push: push:
branches: [ master ] branches: [ master ]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work. fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- uses: dotnet/nbgv@v0.4.0 - uses: dotnet/nbgv@v0.4.0
with: with:
setAllVars: true setAllVars: true
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: 5.0.101 dotnet-version: 5.0.101
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
run: dotnet build --configuration Release --no-restore /p:PublicRelease=true run: dotnet build --configuration Release --no-restore /p:PublicRelease=true
- name: Test - name: Test
run: dotnet test --configuration Release --no-build --verbosity normal run: dotnet test --configuration Release --no-build --verbosity normal
- name: Publish MapTo - name: Publish MapTo
uses: brandedoutcast/publish-nuget@v2.5.5 uses: brandedoutcast/publish-nuget@v2.5.5
with: with:
PROJECT_FILE_PATH: src/BlueWest.MapTo/BlueWest.MapTo.csproj PROJECT_FILE_PATH: src/MapTo/MapTo.csproj
NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_KEY: ${{secrets.NUGET_API_KEY}}
NUGET_SOURCE: https://api.nuget.org NUGET_SOURCE: https://api.nuget.org
TAG_COMMIT: false TAG_COMMIT: false
INCLUDE_SYMBOLS: true INCLUDE_SYMBOLS: true
VERSION_STATIC: ${{env.NBGV_SemVer1}} VERSION_STATIC: ${{env.NBGV_SemVer1}}

884
.gitignore vendored
View File

@ -1,442 +1,442 @@
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
## ##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files # User-specific files
*.rsuser *.rsuser
*.suo *.suo
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs
# Mono auto generated files # Mono auto generated files
mono_crash.* mono_crash.*
# Build results # Build results
[Dd]ebug/ [Dd]ebug/
[Dd]ebugPublic/ [Dd]ebugPublic/
[Rr]elease/ [Rr]elease/
[Rr]eleases/ [Rr]eleases/
x64/ x64/
x86/ x86/
[Aa][Rr][Mm]/ [Aa][Rr][Mm]/
[Aa][Rr][Mm]64/ [Aa][Rr][Mm]64/
bld/ bld/
[Bb]in/ [Bb]in/
[Oo]bj/ [Oo]bj/
[Ll]og/ [Ll]og/
[Ll]ogs/ [Ll]ogs/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
# Uncomment if you have tasks that create the project's static files in wwwroot # Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/ #wwwroot/
# Visual Studio 2017 auto generated files # Visual Studio 2017 auto generated files
Generated\ Files/ Generated\ Files/
# MSTest test Results # MSTest test Results
[Tt]est[Rr]esult*/ [Tt]est[Rr]esult*/
[Bb]uild[Ll]og.* [Bb]uild[Ll]og.*
# NUnit # NUnit
*.VisualState.xml *.VisualState.xml
TestResult.xml TestResult.xml
nunit-*.xml nunit-*.xml
# Build Results of an ATL Project # Build Results of an ATL Project
[Dd]ebugPS/ [Dd]ebugPS/
[Rr]eleasePS/ [Rr]eleasePS/
dlldata.c dlldata.c
# Benchmark Results # Benchmark Results
BenchmarkDotNet.Artifacts/ BenchmarkDotNet.Artifacts/
# .NET Core # .NET Core
project.lock.json project.lock.json
project.fragment.lock.json project.fragment.lock.json
artifacts/ artifacts/
# StyleCop # StyleCop
StyleCopReport.xml StyleCopReport.xml
# Files built by Visual Studio # Files built by Visual Studio
*_i.c *_i.c
*_p.c *_p.c
*_h.h *_h.h
*.ilk *.ilk
*.meta *.meta
*.obj *.obj
*.iobj *.iobj
*.pch *.pch
*.pdb *.pdb
*.ipdb *.ipdb
*.pgc *.pgc
*.pgd *.pgd
*.rsp *.rsp
*.sbr *.sbr
*.tlb *.tlb
*.tli *.tli
*.tlh *.tlh
*.tmp *.tmp
*.tmp_proj *.tmp_proj
*_wpftmp.csproj *_wpftmp.csproj
*.log *.log
*.vspscc *.vspscc
*.vssscc *.vssscc
.builds .builds
*.pidb *.pidb
*.svclog *.svclog
*.scc *.scc
# Chutzpah Test files # Chutzpah Test files
_Chutzpah* _Chutzpah*
# Visual C++ cache files # Visual C++ cache files
ipch/ ipch/
*.aps *.aps
*.ncb *.ncb
*.opendb *.opendb
*.opensdf *.opensdf
*.sdf *.sdf
*.cachefile *.cachefile
*.VC.db *.VC.db
*.VC.VC.opendb *.VC.VC.opendb
# Visual Studio profiler # Visual Studio profiler
*.psess *.psess
*.vsp *.vsp
*.vspx *.vspx
*.sap *.sap
# Visual Studio Trace Files # Visual Studio Trace Files
*.e2e *.e2e
# TFS 2012 Local Workspace # TFS 2012 Local Workspace
$tf/ $tf/
# Guidance Automation Toolkit # Guidance Automation Toolkit
*.gpState *.gpState
# ReSharper is a .NET coding add-in # ReSharper is a .NET coding add-in
_ReSharper*/ _ReSharper*/
*.[Rr]e[Ss]harper *.[Rr]e[Ss]harper
*.DotSettings.user *.DotSettings.user
# TeamCity is a build add-in # TeamCity is a build add-in
_TeamCity* _TeamCity*
# DotCover is a Code Coverage Tool # DotCover is a Code Coverage Tool
*.dotCover *.dotCover
# AxoCover is a Code Coverage Tool # AxoCover is a Code Coverage Tool
.axoCover/* .axoCover/*
!.axoCover/settings.json !.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool # Coverlet is a free, cross platform Code Coverage Tool
coverage*[.json, .xml, .info] coverage*[.json, .xml, .info]
# Visual Studio code coverage results # Visual Studio code coverage results
*.coverage *.coverage
*.coveragexml *.coveragexml
# NCrunch # NCrunch
_NCrunch_* _NCrunch_*
.*crunch*.local.xml .*crunch*.local.xml
nCrunchTemp_* nCrunchTemp_*
# MightyMoose # MightyMoose
*.mm.* *.mm.*
AutoTest.Net/ AutoTest.Net/
# Web workbench (sass) # Web workbench (sass)
.sass-cache/ .sass-cache/
# Installshield output folder # Installshield output folder
[Ee]xpress/ [Ee]xpress/
# DocProject is a documentation generator add-in # DocProject is a documentation generator add-in
DocProject/buildhelp/ DocProject/buildhelp/
DocProject/Help/*.HxT DocProject/Help/*.HxT
DocProject/Help/*.HxC DocProject/Help/*.HxC
DocProject/Help/*.hhc DocProject/Help/*.hhc
DocProject/Help/*.hhk DocProject/Help/*.hhk
DocProject/Help/*.hhp DocProject/Help/*.hhp
DocProject/Help/Html2 DocProject/Help/Html2
DocProject/Help/html DocProject/Help/html
# Click-Once directory # Click-Once directory
publish/ publish/
# Publish Web Output # Publish Web Output
*.[Pp]ublish.xml *.[Pp]ublish.xml
*.azurePubxml *.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings, # Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted # but database connection strings (with potential passwords) will be unencrypted
*.pubxml *.pubxml
*.publishproj *.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to # 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 # checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted # in these scripts will be unencrypted
PublishScripts/ PublishScripts/
# NuGet Packages # NuGet Packages
*.nupkg *.nupkg
# NuGet Symbol Packages # NuGet Symbol Packages
*.snupkg *.snupkg
# The packages folder can be ignored because of Package Restore # The packages folder can be ignored because of Package Restore
**/[Pp]ackages/* **/[Pp]ackages/*
# except build/, which is used as an MSBuild target. # except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/ !**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed # Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config #!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files # NuGet v3's project.json files produces more ignorable files
*.nuget.props *.nuget.props
*.nuget.targets *.nuget.targets
# Microsoft Azure Build Output # Microsoft Azure Build Output
csx/ csx/
*.build.csdef *.build.csdef
# Microsoft Azure Emulator # Microsoft Azure Emulator
ecf/ ecf/
rcf/ rcf/
# Windows Store app package directories and files # Windows Store app package directories and files
AppPackages/ AppPackages/
BundleArtifacts/ BundleArtifacts/
Package.StoreAssociation.xml Package.StoreAssociation.xml
_pkginfo.txt _pkginfo.txt
*.appx *.appx
*.appxbundle *.appxbundle
*.appxupload *.appxupload
# Visual Studio cache files # Visual Studio cache files
# files ending in .cache can be ignored # files ending in .cache can be ignored
*.[Cc]ache *.[Cc]ache
# but keep track of directories ending in .cache # but keep track of directories ending in .cache
!?*.[Cc]ache/ !?*.[Cc]ache/
# Others # Others
ClientBin/ ClientBin/
~$* ~$*
*~ *~
*.dbmdl *.dbmdl
*.dbproj.schemaview *.dbproj.schemaview
*.jfm *.jfm
*.pfx *.pfx
*.publishsettings *.publishsettings
orleans.codegen.cs orleans.codegen.cs
# Including strong name files can present a security risk # Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424) # (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk #*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components # Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/ #bower_components/
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/
# Backup & report files from converting an old project file # Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed, # to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-) # because we have git ;-)
_UpgradeReport_Files/ _UpgradeReport_Files/
Backup*/ Backup*/
UpgradeLog*.XML UpgradeLog*.XML
UpgradeLog*.htm UpgradeLog*.htm
ServiceFabricBackup/ ServiceFabricBackup/
*.rptproj.bak *.rptproj.bak
# SQL Server files # SQL Server files
*.mdf *.mdf
*.ldf *.ldf
*.ndf *.ndf
# Business Intelligence projects # Business Intelligence projects
*.rdl.data *.rdl.data
*.bim.layout *.bim.layout
*.bim_*.settings *.bim_*.settings
*.rptproj.rsuser *.rptproj.rsuser
*- [Bb]ackup.rdl *- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes # Microsoft Fakes
FakesAssemblies/ FakesAssemblies/
# GhostDoc plugin setting file # GhostDoc plugin setting file
*.GhostDoc.xml *.GhostDoc.xml
# Node.js Tools for Visual Studio # Node.js Tools for Visual Studio
.ntvs_analysis.dat .ntvs_analysis.dat
node_modules/ node_modules/
# Visual Studio 6 build log # Visual Studio 6 build log
*.plg *.plg
# Visual Studio 6 workspace options file # Visual Studio 6 workspace options file
*.opt *.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw *.vbw
# Visual Studio LightSwitch build output # Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts **/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml **/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts **/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml **/*.Server/ModelManifest.xml
_Pvt_Extensions _Pvt_Extensions
# Paket dependency manager # Paket dependency manager
.paket/paket.exe .paket/paket.exe
paket-files/ paket-files/
# FAKE - F# Make # FAKE - F# Make
.fake/ .fake/
# CodeRush personal settings # CodeRush personal settings
.cr/personal .cr/personal
# Python Tools for Visual Studio (PTVS) # Python Tools for Visual Studio (PTVS)
__pycache__/ __pycache__/
*.pyc *.pyc
# Cake - Uncomment if you are using it # Cake - Uncomment if you are using it
# tools/** # tools/**
# !tools/packages.config # !tools/packages.config
# Tabs Studio # Tabs Studio
*.tss *.tss
# Telerik's JustMock configuration file # Telerik's JustMock configuration file
*.jmconfig *.jmconfig
# BizTalk build output # BizTalk build output
*.btp.cs *.btp.cs
*.btm.cs *.btm.cs
*.odx.cs *.odx.cs
*.xsd.cs *.xsd.cs
# OpenCover UI analysis results # OpenCover UI analysis results
OpenCover/ OpenCover/
# Azure Stream Analytics local run output # Azure Stream Analytics local run output
ASALocalRun/ ASALocalRun/
# MSBuild Binary and Structured Log # MSBuild Binary and Structured Log
*.binlog *.binlog
# NVidia Nsight GPU debugger configuration file # NVidia Nsight GPU debugger configuration file
*.nvuser *.nvuser
# MFractors (Xamarin productivity tool) working folder # MFractors (Xamarin productivity tool) working folder
.mfractor/ .mfractor/
# Local History for Visual Studio # Local History for Visual Studio
.localhistory/ .localhistory/
# BeatPulse healthcheck temp database # BeatPulse healthcheck temp database
healthchecksdb healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017 # Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder # Ionide (cross platform F# VS Code tools) working folder
.ionide/ .ionide/
## ##
## Visual studio for Mac ## Visual studio for Mac
## ##
# globs # globs
Makefile.in Makefile.in
*.userprefs *.userprefs
*.usertasks *.usertasks
config.make config.make
config.status config.status
aclocal.m4 aclocal.m4
install-sh install-sh
autom4te.cache/ autom4te.cache/
*.tar.gz *.tar.gz
tarballs/ tarballs/
test-results/ test-results/
# Mac bundle stuff # Mac bundle stuff
*.dmg *.dmg
*.app *.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General # General
.DS_Store .DS_Store
.AppleDouble .AppleDouble
.LSOverride .LSOverride
# Icon must end with two \r # Icon must end with two \r
Icon Icon
# Thumbnails # Thumbnails
._* ._*
# Files that might appear in the root of a volume # Files that might appear in the root of a volume
.DocumentRevisions-V100 .DocumentRevisions-V100
.fseventsd .fseventsd
.Spotlight-V100 .Spotlight-V100
.TemporaryItems .TemporaryItems
.Trashes .Trashes
.VolumeIcon.icns .VolumeIcon.icns
.com.apple.timemachine.donotpresent .com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share # Directories potentially created on remote AFP share
.AppleDB .AppleDB
.AppleDesktop .AppleDesktop
Network Trash Folder Network Trash Folder
Temporary Items Temporary Items
.apdisk .apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files # Windows thumbnail cache files
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
ehthumbs_vista.db ehthumbs_vista.db
# Dump file # Dump file
*.stackdump *.stackdump
# Folder config file # Folder config file
[Dd]esktop.ini [Dd]esktop.ini
# Recycle Bin used on file shares # Recycle Bin used on file shares
$RECYCLE.BIN/ $RECYCLE.BIN/
# Windows Installer files # Windows Installer files
*.cab *.cab
*.msi *.msi
*.msix *.msix
*.msm *.msm
*.msp *.msp
# Windows shortcuts # Windows shortcuts
*.lnk *.lnk
# JetBrains Rider # JetBrains Rider
.idea/ .idea/
*.sln.iml *.sln.iml
## ##
## Visual Studio Code ## Visual Studio Code
## ##
.vscode/* .vscode/*
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json

40
LICENSE
View File

@ -1,21 +1,21 @@
MIT License MIT License
Copyright (c) 2020 Mohammadreza Taikandi Copyright (c) 2020 Mohammadreza Taikandi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

View File

@ -1,34 +1,34 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\BlueWest.MapTo\BlueWest.MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\MapTo\MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Tests\MapTo.Tests.csproj", "{797DA57B-AC7E-468B-8799-44C5A574C0E3}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Tests\MapTo.Tests.csproj", "{797DA57B-AC7E-468B-8799-44C5A574C0E3}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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}.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.ActiveCfg = Release|Any CPU
{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{797DA57B-AC7E-468B-8799-44C5A574C0E3}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.Build.0 = Release|Any CPU {23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,6 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mapto/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Mapto/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=paramref/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=paramref/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Shouldly/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Shouldly/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=typeparam/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=typeparam/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Usings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Usings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

180
README.md
View File

@ -1,91 +1,91 @@
# MapTo # MapTo
[![Nuget](https://img.shields.io/nuget/v/mapto?logo=nuget)](https://www.nuget.org/packages/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) ![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). 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. 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 ## Installation
``` ```
dotnet add package MapTo --prerelease dotnet add package MapTo --prerelease
``` ```
## Usage ## 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. 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# ```c#
using MapTo; using MapTo;
namespace App.ViewModels namespace App.ViewModels
{ {
[MapFrom(typeof(App.Data.Models.User))] [MapFrom(typeof(App.Data.Models.User))]
public partial class UserViewModel public partial class UserViewModel
{ {
public string FirstName { get; } public string FirstName { get; }
public string LastName { get; } public string LastName { get; }
[IgnoreProperty] [IgnoreProperty]
public string FullName { get; set; } public string FullName { get; set; }
} }
} }
``` ```
To get an instance of `UserViewModel` from the `User` class, you can use any of the following methods: To get an instance of `UserViewModel` from the `User` class, you can use any of the following methods:
```c# ```c#
var user = new User(id: 10) { FirstName = "John", LastName = "Doe" }; var user = new User(id: 10) { FirstName = "John", LastName = "Doe" };
var vm = user.ToUserViewModel(); // A generated extension method for User class. var vm = user.ToUserViewModel(); // A generated extension method for User class.
// OR // OR
vm = new UserViewModel(user); // A generated contructor. vm = new UserViewModel(user); // A generated contructor.
// OR // OR
vm = UserViewModel.From(user); // A generated factory method. 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. > Please refer to [sample console app](https://github.com/mrtaikandi/MapTo/tree/master/test/TestConsoleApp) for a more complete example.
## Available Attributes ## Available Attributes
### IgnoreProperty ### 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. 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# ```c#
[IgnoreProperty] [IgnoreProperty]
public string FullName { get; set; } public string FullName { get; set; }
``` ```
### MapProperty ### 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. 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# ```c#
[MapProperty(SourcePropertyName = "Id")] [MapProperty(SourcePropertyName = "Id")]
public int Key { get; set; } public int Key { get; set; }
``` ```
### MapTypeConverter ### 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. 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<in TSource, out TDestination>` interface. This attribute will accept a type that implements `ITypeConverter<in TSource, out TDestination>` interface.
```c# ```c#
[MapFrom(typeof(User))] [MapFrom(typeof(User))]
public partial class UserViewModel public partial class UserViewModel
{ {
public DateTimeOffset RegisteredAt { get; set; } public DateTimeOffset RegisteredAt { get; set; }
[IgnoreProperty] [IgnoreProperty]
public ProfileViewModel Profile { get; set; } public ProfileViewModel Profile { get; set; }
[MapTypeConverter(typeof(IdConverter))] [MapTypeConverter(typeof(IdConverter))]
[MapProperty(SourcePropertyName = nameof(User.Id))] [MapProperty(SourcePropertyName = nameof(User.Id))]
public string Key { get; } public string Key { get; }
private class IdConverter : ITypeConverter<int, string> private class IdConverter : ITypeConverter<int, string>
{ {
public string Convert(int source, object[] converterParameters) => $"{source:X}"; public string Convert(int source, object[] converterParameters) => $"{source:X}";
} }
} }
``` ```

View File

@ -1,67 +0,0 @@
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<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapFieldSimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,86 +0,0 @@
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<object> 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)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
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 {targetSourceType.SourceNamespace}")
.WriteComment($" SourceTypeFullName {targetSourceType.SourceTypeFullName}")
.WriteComment($" SourceTypeIdentifierName {targetSourceType.SourceTypeIdentifierName}");
}
return builder;
}
internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray<MappedMember> 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;
}
}
}

View File

@ -1,379 +0,0 @@
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;
#pragma warning disable CS8602
namespace MapTo.Extensions
{
internal static class CommonSource
{
internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass)
{
const bool writeDebugInfo = false;
List<string> constructorHeaders = new List<string>();
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket();
builder
// Class declaration
.WriteLine($"partial {structOrClass} {model.TypeIdentifierName}")
.WriteOpeningBracket()
.WriteLine()
// Class body
.GeneratePublicEmptyConstructor(model, ref constructorHeaders, true)
.GeneratePublicConstructor(model, ref constructorHeaders)
.GeneratePublicConstructor(model, ref constructorHeaders, true);
if (model.IsTypeUpdatable) builder.GenerateUpdateMethod(model);
if (model.IsJsonExtension) builder.WriteToJsonMethod(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, ref List<string> constructorHeaders, bool filterNonMapped = false)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
var stringBuilder = new StringBuilder();
var otherProperties = new List<MappedMember>();
foreach (var property in targetSourceType.TypeProperties)
{
if (!targetSourceType.SourceProperties.IsMappedProperty(property) && !filterNonMapped)
{
stringBuilder.Append(", ");
stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
otherProperties.Add(property);
}
}
foreach (var property in targetSourceType.TypeFields)
{
if (!targetSourceType.SourceFields.IsMappedProperty(property) && !filterNonMapped)
{
stringBuilder.Append(", ");
stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
otherProperties.Add(property);
}
}
var readOnlyPropertiesArguments = stringBuilder.ToString();
var constructorHeader =
$"public {model.TypeIdentifierName}({targetSourceType.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}";
bool hasAlreadyConstructor = false;
foreach (var header in constructorHeaders)
{
if(constructorHeader.Contains(header)) hasAlreadyConstructor = true;
}
if (hasAlreadyConstructor) continue;
constructorHeaders.Add(constructorHeader);
builder
.WriteLine(constructorHeader)
.WriteOpeningBracket()
.WriteAssignmentMethod(model, filterNonMapped ? null : otherProperties.ToArray().ToImmutableArray(), sourceClassParameterName, mappingContextParameterName, filterNonMapped);
builder.WriteClosingBracket()
.WriteLine();
}
// End constructor declaration
return builder;
}
private static SourceBuilder GeneratePublicEmptyConstructor(this SourceBuilder builder, MappingModel model, ref List<string> constructorHeaders, bool filterNonMapped = false)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
var constructorHeader =
$"public {model.TypeIdentifierName}(){baseConstructor}";
bool hasAlreadyConstructor = false;
foreach (var header in constructorHeaders)
{
if(constructorHeader.Contains(header)) hasAlreadyConstructor = true;
}
if (hasAlreadyConstructor) continue;
constructorHeaders.Add(constructorHeader);
builder
.WriteLine(constructorHeader)
.WriteOpeningBracket()
.WriteClosingBracket()
.WriteLine();
}
// End constructor declaration
return builder;
}
private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray<MappedMember> 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 targetSourceType in model.MappedSourceTypes)
{
foreach (var property in targetSourceType.TypeProperties)
{
if (!property.isEnumerable)
HandlePropertyEnumerable(builder, property);
else
{
builder = WriteJsonField(builder, property);
}
}
foreach (var property in targetSourceType.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;
#pragma warning disable CS8602
builder.WriteCommentArray(symbol.Parameters, nameof(symbol.Parameters));
#pragma warning restore CS8602
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<MappedMember>? otherProperties,
string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate)
{
List<MappedMember> _addedMembers = new List<MappedMember>();
foreach (var targetSourceType in model.MappedSourceTypes)
{
foreach (var property in targetSourceType.SourceProperties)
{
if (property.isReadOnly && fromUpdate) continue;
if(_addedMembers.Contains(property)) continue;
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
_addedMembers.Add(property);
}
foreach (var property in targetSourceType.SourceFields)
{
if (property.isReadOnly && fromUpdate) continue;
if(_addedMembers.Contains(property)) continue;
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
_addedMembers.Add(property);
}
if (otherProperties == null) return builder;
foreach (var property in otherProperties)
{
if(_addedMembers.Contains(property)) continue;
builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};"
: "");
_addedMembers.Add(property);
}
}
return builder;
}
private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName, targetSourceType)
.WriteLine($"public void Update({targetSourceType.SourceType} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteAssignmentMethod(model, null, sourceClassParameterName, "context", true)
.WriteClosingBracket()
.WriteLine();
}
return builder;
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
MappedSourceType mappedSourceType)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{mappedSourceType.SourceType}\"/> to use as source.</param>");
return builder;
}
private static SourceBuilder GenerateEnumerableJsonSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static string ToJson(this IEnumerable<{targetSourceType.SourceType}{model.Options.NullableReferenceSyntax}> {sourceClassParameterName}List)")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
}
}

View File

@ -1,630 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
#pragma warning disable CS8602
namespace MapTo
{
internal static class MappingContextExtensions
{
internal static ImmutableArray<MappedMember> GetReadOnlyMappedProperties(this ImmutableArray<MappedMember> mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!;
internal static ImmutableArray<MappedMember> GetWritableMappedProperties(this ImmutableArray<MappedMember> mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!;
}
internal abstract class MappingContext
{
private readonly List<SymbolDisplayPart> _ignoredNamespaces;
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
_ignoredNamespaces = new();
Diagnostics = ImmutableArray<Diagnostic>.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<Diagnostic> 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; } = null!;
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<string> 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<IPropertySymbol> sourceProperties, ISymbol property)
{
var propertyName = property
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.FirstOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.FirstOrDefault(p => p.Name == propertyName);
}
protected IFieldSymbol? FindSourceField(IEnumerable<IFieldSymbol> sourceProperties, ISymbol property)
{
var propertyName = property
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.FirstOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.FirstOrDefault(p => p.Name == propertyName);
}
protected abstract ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected ImmutableArray<INamedTypeSymbol> GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null)
{
var attributeData = typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName);
var sourceSymbol = GetSourceTypeSymbol(attributeData, semanticModel);
return sourceSymbol;
}
// we need two possible InamedTypeSymbol
protected ImmutableArray<INamedTypeSymbol> GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
{
if (attributeSyntax is null)
{
return new ImmutableArray<INamedTypeSymbol>(){};
}
semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
var descendentNodes = attributeSyntax
.DescendantNodes();
var sourceTypeExpressionSyntax = descendentNodes
.OfType<TypeOfExpressionSyntax>()
.ToImmutableArray();
// cast
var resultList = new List<INamedTypeSymbol>();
for (int i = 0; i < sourceTypeExpressionSyntax.Length; i++)
{
var sourceTypeExpression = sourceTypeExpressionSyntax[i];
if (semanticModel.GetTypeInfo(sourceTypeExpression.Type).Type is INamedTypeSymbol namedTypeSymbol)
{
resultList.Add(namedTypeSymbol);
}
}
return resultList.ToImmutableArray();
}
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<IPropertySymbol> 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<string>.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<IFieldSymbol> 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<string>.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() ?? throw new InvalidOperationException(),
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<string>.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,
#pragma warning disable CS8602
(property as IPropertySymbol).IsReadOnly);
#pragma warning restore CS8602
;
}
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<string>.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<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.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<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.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<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
{
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
return converterParameter.IsNull
? ImmutableArray<string>.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;
}
// We can have 2 sources...
var sourceTypeSymbols = GetSourceTypeSymbol(TypeSyntax, semanticModel);
// lets pick one for now, and then think what to do with the second one
if (sourceTypeSymbols.IsDefaultOrEmpty)
{
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation()));
return null;
}
var typeIdentifierName = TypeSyntax.GetIdentifierName();
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var isTypeUpdatable = true; //IsTypeUpdatable();
var hasJsonExtension = false; // HasJsonExtension();
List<MappedSourceType> mappedSourceTypes = new List<MappedSourceType>();
foreach (var sourceTypeSymbol in sourceTypeSymbols)
{
_ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First());
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
var mappedFields = GetSourceMappedFields(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass);
var allFields = GetTypeMappedFields(sourceTypeSymbol, typeSymbol, isTypeInheritFromMappedBaseClass);
mappedSourceTypes.Add(new MappedSourceType(
sourceTypeSymbol.ContainingNamespace.ToDisplayString(),
sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(),
mappedProperties, mappedFields, allProperties, allFields, shouldGenerateSecondaryConstructor));
}
//var sourceTypeSymbol = sourceTypeSymbols[0];
// Pick first one to avoid errors. TODO: Make possible to use different source types
/*if (!mappedProperties.Any())
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
return null;
}*/
return new MappingModel(
SourceGenerationOptions,
TypeSyntax.GetNamespace(),
TypeSyntax.Modifiers,
TypeSyntax.Keyword.Text,
typeIdentifierName,
isTypeUpdatable,
hasJsonExtension,
mappedSourceTypes.ToImmutableArray(),
isTypeInheritFromMappedBaseClass,
Usings);
}
private INamedTypeSymbol? GetTypeConverterBaseInterfaceForProperty(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
return converterTypeSymbol.AllInterfaces
.FirstOrDefault(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
.FirstOrDefault(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<ConstructorDeclarationSyntax>()
.FirstOrDefault(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;
}
}
}

View File

@ -1,222 +0,0 @@
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<string> TypeConverterParameters,
string SourcePropertyName,
string? MappedSourcePropertyTypeName,
string? EnumerableTypeArgument,
ISymbol ActualSymbol,
INamedTypeSymbol? NamedTypeSymbol,
bool isEnumerable,
bool isReadOnly)
{
public bool IsEnumerable => EnumerableTypeArgument is not null;
}
internal record MappedSourceType
(
string SourceNamespace,
string SourceTypeIdentifierName,
string SourceTypeFullName,
ImmutableArray<MappedMember> SourceProperties,
ImmutableArray<MappedMember> SourceFields,
ImmutableArray<MappedMember> TypeProperties,
ImmutableArray<MappedMember> TypeFields,
bool GenerateSecondaryConstructor
)
{
public string SourceType => SourceTypeFullName;
}
internal record MappingModel(
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList Modifiers,
string Type,
string TypeIdentifierName,
bool IsTypeUpdatable,
bool IsJsonExtension,
ImmutableArray<MappedSourceType> MappedSourceTypes,
bool HasMappedBaseClass,
ImmutableArray<string> Usings
);
internal interface IEfMethodsModel { }
internal record EfMethodsModel(
SourceGenerationOptions Options,
string Namespace,
string ContextTypeName,
string ContextFullType,
ImmutableArray<EfEntityDataModel> MethodsModels,
ImmutableArray<string> Usings
);
internal class EfEntityDataModel
{
public string PropertyName { get; set; }
public string EntityTypeFullName { get; set; }
public string EntityTypeIdentifierName { get; set; }
public EfEntityDataModel(string propertyName, string entityTypeFullName, string entityTypeIdentifierName)
{
PropertyName = propertyName;
EntityTypeFullName = entityTypeFullName;
EntityTypeIdentifierName = entityTypeIdentifierName;
}
}
internal class EfAddMethodsModel : EfEntityDataModel
{
public string CreateTypeFullName { get; set; }
public string CreateTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public string ReturnTypeIdentifierName { get; set; }
public EfAddMethodsModel(EfEntityDataModel entity, string createTypeFullName,
string createTypeIdentifierName,
string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
CreateTypeFullName = createTypeFullName;
CreateTypeIdentifierName = createTypeIdentifierName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfGetOneByModel : EfEntityDataModel
{
public string ByParamPropertyName { get; set; }
public string ByParamFullTypeName { get; set; }
public string ByParamTypeName { get; set; }
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetOneByModel(EfEntityDataModel entity, string byParamPropertyName,
string byParamFullTypeName,
string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ByParamPropertyName = byParamPropertyName;
ByParamFullTypeName = byParamFullTypeName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
ByParamTypeName = returnTypeFullName;
}
}
internal class EfGetOneWithModel : EfEntityDataModel
{
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetOneWithModel(EfEntityDataModel entity, string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfGetManyModel : EfEntityDataModel
{
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetManyModel(EfEntityDataModel entity, string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfUpdateMethodsModel : EfEntityDataModel
{
public string UpdateTypeFullName;
public string UpdateTypeIdentifierName;
public string ReturnTypeFullName;
public string ReturnTypeIdentifierName;
public string KeyPropertyName;
public string KeyFullTypeName;
public EfUpdateMethodsModel(EfEntityDataModel entity,
string updateTypeFullName,
string updateTypeIdentifierName,
string returnTypeFullName,
string returnTypeIdentifierName,
string keyPropertyName,
string keyFullTypeName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
UpdateTypeFullName = updateTypeFullName;
UpdateTypeIdentifierName = updateTypeIdentifierName;
ReturnTypeFullName= returnTypeFullName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
KeyPropertyName = keyPropertyName;
KeyFullTypeName = keyFullTypeName;
}
}
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;
}
}

View File

@ -1,52 +0,0 @@
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<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
throw new System.NotImplementedException();
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.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<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
throw new System.NotImplementedException();
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.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()!;
}
}
}

View File

@ -1,60 +0,0 @@
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("/// <summary>")
.WriteLine("/// Specifies the mapping behavior of the annotated property.")
.WriteLine("/// </summary>")
.WriteLine("/// <remarks>")
.WriteLine($"/// {AttributeClassName} has a number of uses:")
.WriteLine("/// <list type=\"bullet\">")
.WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>")
.WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>")
.WriteLine("/// </list>")
.WriteLine("/// </remarks>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets or sets the property name of the object to mapping from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public string{options.NullableReferenceSyntax} {SourceMemberNameFieldOrPropertyName} {{ get; set; }}")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -1,36 +0,0 @@
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("/// <summary>")
.WriteLine("/// Specifies that the annotated property should be excluded.")
.WriteLine("/// </summary>");
}
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");
}
}
}

View File

@ -1,40 +0,0 @@
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("/// <summary>")
.WriteLine("/// Specifies that the annotated class has a json extension.")
.WriteLine("/// </summary>");
}
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");
}
}
}

View File

@ -1,14 +0,0 @@
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");
}
}
}

View File

@ -1,235 +0,0 @@
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();
foreach (var targetSourceType in model.MappedSourceTypes)
{
if (targetSourceType.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
}
// Class body
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)
{
// grab first data from array
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
builder .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({targetSourceType.SourceType} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
return builder;
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.WriteLine(
$"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {targetSourceType.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);")
.WriteClosingBracket();
}
// End constructor declaration
return builder;
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
string mappingContextParameterName)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
for (var i = 0; i < targetSourceType.SourceProperties.Length; i++)
{
var property = targetSourceType.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 < targetSourceType.SourceProperties.Length - 1)
{
builder.Write(", ");
}
}
}
return builder;
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({targetSourceType.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine(
$"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{targetSourceType.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{targetSourceType.SourceType}\"/> to use as source.</param>")
.WriteLine(
$"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
return builder;
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {targetSourceType.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
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 {targetSourceType.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
}
}

View File

@ -1,15 +0,0 @@
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");
}
}
}

View File

@ -1,40 +0,0 @@
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("/// <summary>")
.WriteLine("/// Specifies that the annotated class can be updatable.")
.WriteLine("/// </summary>");
}
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");
}
}
}

View File

@ -1,67 +0,0 @@
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<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapFieldSimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -0,0 +1,27 @@
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<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers(!isInheritFromMappedBaseClass)
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,16 +1,16 @@
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
// ReSharper disable CheckNamespace // ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel; using System.ComponentModel;
namespace System.Runtime.CompilerServices namespace System.Runtime.CompilerServices
{ {
/// <summary> /// <summary>
/// Reserved to be used by the compiler for tracking metadata. /// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code. /// This class should not be used by developers in source code.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { } internal static class IsExternalInit { }
} }

View File

@ -1,178 +1,178 @@
// ReSharper disable CheckNamespace // ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
#if NETSTANDARD2_0 #if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis namespace System.Diagnostics.CodeAnalysis
{ {
// These attributes already shipped with .NET Core 3.1 in System.Runtime // These attributes already shipped with .NET Core 3.1 in System.Runtime
#if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1 #if !NETCOREAPP3_0 && !NETCOREAPP3_1 && !NETSTANDARD2_1
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary> /// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
internal sealed class AllowNullAttribute : Attribute { } internal sealed class AllowNullAttribute : Attribute { }
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary> /// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property)]
internal sealed class DisallowNullAttribute : Attribute { } internal sealed class DisallowNullAttribute : Attribute { }
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary> /// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
internal sealed class MaybeNullAttribute : Attribute { } internal sealed class MaybeNullAttribute : Attribute { }
/// <summary> /// <summary>
/// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input /// 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. /// argument was not null when the call returns.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
internal sealed class NotNullAttribute : Attribute { } internal sealed class NotNullAttribute : Attribute { }
/// <summary> /// <summary>
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter may be null even if the /// Specifies that when a method returns <see cref="ReturnValue" />, the parameter may be null even if the
/// corresponding type disallows it. /// corresponding type disallows it.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
internal sealed class MaybeNullWhenAttribute : Attribute internal sealed class MaybeNullWhenAttribute : Attribute
{ {
/// <summary>Initializes the attribute with the specified return value condition.</summary> /// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue"> /// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter may be null. /// The return value condition. If the method returns this value, the associated parameter may be null.
/// </param> /// </param>
public MaybeNullWhenAttribute(bool returnValue) public MaybeNullWhenAttribute(bool returnValue)
{ {
ReturnValue = returnValue; ReturnValue = returnValue;
} }
/// <summary>Gets the return value condition.</summary> /// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; } public bool ReturnValue { get; }
} }
/// <summary> /// <summary>
/// Specifies that when a method returns <see cref="ReturnValue" />, the parameter will not be null even if the /// Specifies that when a method returns <see cref="ReturnValue" />, the parameter will not be null even if the
/// corresponding type allows it. /// corresponding type allows it.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
internal sealed class NotNullWhenAttribute : Attribute internal sealed class NotNullWhenAttribute : Attribute
{ {
/// <summary>Initializes the attribute with the specified return value condition.</summary> /// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue"> /// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null. /// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param> /// </param>
public NotNullWhenAttribute(bool returnValue) public NotNullWhenAttribute(bool returnValue)
{ {
ReturnValue = returnValue; ReturnValue = returnValue;
} }
/// <summary>Gets the return value condition.</summary> /// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; } public bool ReturnValue { get; }
} }
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary> /// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)]
internal sealed class NotNullIfNotNullAttribute : Attribute internal sealed class NotNullIfNotNullAttribute : Attribute
{ {
/// <summary>Initializes the attribute with the associated parameter name.</summary> /// <summary>Initializes the attribute with the associated parameter name.</summary>
/// <param name="parameterName"> /// <param name="parameterName">
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
/// </param> /// </param>
public NotNullIfNotNullAttribute(string parameterName) public NotNullIfNotNullAttribute(string parameterName)
{ {
ParameterName = parameterName; ParameterName = parameterName;
} }
/// <summary>Gets the associated parameter name.</summary> /// <summary>Gets the associated parameter name.</summary>
public string ParameterName { get; } public string ParameterName { get; }
} }
/// <summary>Applied to a method that will never return under any circumstance.</summary> /// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)] [AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute { } internal sealed class DoesNotReturnAttribute : Attribute { }
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary> /// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
[AttributeUsage(AttributeTargets.Parameter)] [AttributeUsage(AttributeTargets.Parameter)]
internal sealed class DoesNotReturnIfAttribute : Attribute internal sealed class DoesNotReturnIfAttribute : Attribute
{ {
/// <summary>Initializes the attribute with the specified parameter value.</summary> /// <summary>Initializes the attribute with the specified parameter value.</summary>
/// <param name="parameterValue"> /// <param name="parameterValue">
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
/// the associated parameter matches this value. /// the associated parameter matches this value.
/// </param> /// </param>
public DoesNotReturnIfAttribute(bool parameterValue) public DoesNotReturnIfAttribute(bool parameterValue)
{ {
ParameterValue = parameterValue; ParameterValue = parameterValue;
} }
/// <summary>Gets the condition parameter value.</summary> /// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; } public bool ParameterValue { get; }
} }
#endif #endif
/// <summary> /// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have not-null /// Specifies that the method or property will ensure that the listed field and property members have not-null
/// values. /// values.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullAttribute : Attribute internal sealed class MemberNotNullAttribute : Attribute
{ {
/// <summary>Initializes the attribute with a field or property member.</summary> /// <summary>Initializes the attribute with a field or property member.</summary>
/// <param name="member"> /// <param name="member">
/// The field or property member that is promised to be not-null. /// The field or property member that is promised to be not-null.
/// </param> /// </param>
public MemberNotNullAttribute(string member) public MemberNotNullAttribute(string member)
{ {
Members = new[] { member }; Members = new[] { member };
} }
/// <summary>Initializes the attribute with the list of field and property members.</summary> /// <summary>Initializes the attribute with the list of field and property members.</summary>
/// <param name="members"> /// <param name="members">
/// The list of field and property members that are promised to be not-null. /// The list of field and property members that are promised to be not-null.
/// </param> /// </param>
public MemberNotNullAttribute(params string[] members) public MemberNotNullAttribute(params string[] members)
{ {
Members = members; Members = members;
} }
/// <summary>Gets field or property member names.</summary> /// <summary>Gets field or property member names.</summary>
public string[] Members { get; } public string[] Members { get; }
} }
/// <summary> /// <summary>
/// Specifies that the method or property will ensure that the listed field and property members have not-null /// 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. /// values when returning with the specified return value condition.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullWhenAttribute : Attribute internal sealed class MemberNotNullWhenAttribute : Attribute
{ {
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary> /// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
/// <param name="returnValue"> /// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null. /// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param> /// </param>
/// <param name="member"> /// <param name="member">
/// The field or property member that is promised to be not-null. /// The field or property member that is promised to be not-null.
/// </param> /// </param>
public MemberNotNullWhenAttribute(bool returnValue, string member) public MemberNotNullWhenAttribute(bool returnValue, string member)
{ {
ReturnValue = returnValue; ReturnValue = returnValue;
Members = new[] { member }; Members = new[] { member };
} }
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary> /// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
/// <param name="returnValue"> /// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null. /// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param> /// </param>
/// <param name="members"> /// <param name="members">
/// The list of field and property members that are promised to be not-null. /// The list of field and property members that are promised to be not-null.
/// </param> /// </param>
public MemberNotNullWhenAttribute(bool returnValue, params string[] members) public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{ {
ReturnValue = returnValue; ReturnValue = returnValue;
Members = members; Members = members;
} }
/// <summary>Gets field or property member names.</summary> /// <summary>Gets field or property member names.</summary>
public string[] Members { get; } public string[] Members { get; }
/// <summary>Gets the return value condition.</summary> /// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; } public bool ReturnValue { get; }
} }
} }
#endif #endif

View File

@ -1,42 +1,42 @@
using System.Linq; using System.Linq;
using MapTo.Extensions; using MapTo.Extensions;
using MapTo.Sources; using MapTo.Sources;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
namespace MapTo namespace MapTo
{ {
internal static class DiagnosticsFactory internal static class DiagnosticsFactory
{ {
private const string UsageCategory = "Usage"; private const string UsageCategory = "Usage";
private const string CodePrefix = "MT"; private const string CodePrefix = "MT";
private const string ErrorId = CodePrefix + "0"; private const string ErrorId = CodePrefix + "0";
private const string InfoId = CodePrefix + "1"; private const string InfoId = CodePrefix + "1";
private const string WarningId = CodePrefix + "2"; private const string WarningId = CodePrefix + "2";
internal static Diagnostic TypeNotFoundError(Location location, string syntaxName) => internal static Diagnostic TypeNotFoundError(Location location, string syntaxName) =>
Create($"{ErrorId}010", location, $"Unable to find '{syntaxName}' type."); Create($"{ErrorId}010", location, $"Unable to find '{syntaxName}' type.");
internal static Diagnostic MapFromAttributeNotFoundError(Location location) => internal static Diagnostic MapFromAttributeNotFoundError(Location location) =>
Create($"{ErrorId}020", location, $"Unable to find {MapFromAttributeSource.AttributeName} type."); Create($"{ErrorId}020", location, $"Unable to find {MapFromAttributeSource.AttributeName} type.");
internal static Diagnostic NoMatchingPropertyFoundError(Location location, INamedTypeSymbol classType, INamedTypeSymbol sourceType) => 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."); Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types.");
internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) => 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}'."); 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) => 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()}>'."); 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) => internal static Diagnostic ConfigurationParseError(string error) =>
Create($"{ErrorId}040", Location.None, error); Create($"{ErrorId}040", Location.None, error);
internal static Diagnostic MissingConstructorArgument(ConstructorDeclarationSyntax constructorSyntax) => internal static Diagnostic MissingConstructorArgument(ConstructorDeclarationSyntax constructorSyntax) =>
Create($"{ErrorId}050", constructorSyntax.GetLocation(), "There are no argument given that corresponds to the required formal parameter."); 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) => 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); Diagnostic.Create(new DiagnosticDescriptor(id, string.Empty, message, UsageCategory, severity, true), location ?? Location.None);
} }
} }

View File

@ -1,9 +1,9 @@
using System; using System;
namespace MapTo.Extensions namespace MapTo.Extensions
{ {
internal static class EnumExtensions internal static class EnumExtensions
{ {
internal static string ToLowercaseString(this Enum member) => member.ToString().ToLower(); internal static string ToLowercaseString(this Enum member) => member.ToString().ToLower();
} }
} }

View File

@ -1,19 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace MapTo.Extensions namespace MapTo.Extensions
{ {
internal static class EnumerableExtensions internal static class EnumerableExtensions
{ {
internal static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action) internal static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
{ {
foreach (var item in enumerable) foreach (var item in enumerable)
{ {
action(item); action(item);
} }
} }
internal static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable.Any(); internal static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable.Any();
} }
} }

View File

@ -1,51 +1,51 @@
using System; using System;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
namespace MapTo.Extensions namespace MapTo.Extensions
{ {
internal static class GeneratorExecutionContextExtensions internal static class GeneratorExecutionContextExtensions
{ {
private const string PropertyNameSuffix = "MapTo_"; private const string PropertyNameSuffix = "MapTo_";
internal static T GetBuildGlobalOption<T>(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T : notnull internal static T GetBuildGlobalOption<T>(this GeneratorExecutionContext context, string propertyName, T defaultValue = default!) where T : notnull
{ {
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue) || string.IsNullOrWhiteSpace(optionValue)) if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(GetBuildPropertyName(propertyName), out var optionValue) || string.IsNullOrWhiteSpace(optionValue))
{ {
return defaultValue; return defaultValue;
} }
var type = typeof(T); var type = typeof(T);
if (!type.IsEnum) if (!type.IsEnum)
{ {
return (T)Convert.ChangeType(optionValue, type); return (T)Convert.ChangeType(optionValue, type);
} }
try try
{ {
return (T)Enum.Parse(type, optionValue, true); return (T)Enum.Parse(type, optionValue, true);
} }
catch (Exception) catch (Exception)
{ {
context.ReportDiagnostic(DiagnosticsFactory.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property.")); context.ReportDiagnostic(DiagnosticsFactory.ConfigurationParseError($"'{optionValue}' is not a valid value for {PropertyNameSuffix}{propertyName} property."));
return defaultValue; return defaultValue;
} }
} }
internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}"; internal static string GetBuildPropertyName(string propertyName) => $"build_property.{PropertyNameSuffix}{propertyName}";
internal static Compilation AddSource(this Compilation compilation, ref GeneratorExecutionContext context, SourceCode sourceCode) internal static Compilation AddSource(this Compilation compilation, ref GeneratorExecutionContext context, SourceCode sourceCode)
{ {
var sourceText = SourceText.From(sourceCode.Text, Encoding.UTF8); var sourceText = SourceText.From(sourceCode.Text, Encoding.UTF8);
context.AddSource(sourceCode.HintName, sourceText); context.AddSource(sourceCode.HintName, sourceText);
// NB: https://github.com/dotnet/roslyn/issues/49753 // NB: https://github.com/dotnet/roslyn/issues/49753
// To be replaced after above issue is resolved. // To be replaced after above issue is resolved.
var options = (CSharpParseOptions)((CSharpCompilation)compilation).SyntaxTrees[0].Options; var options = (CSharpParseOptions)((CSharpCompilation)compilation).SyntaxTrees[0].Options;
return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceText, options)); return compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(sourceText, options));
} }
} }
} }

View File

@ -1,136 +1,131 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo.Extensions namespace MapTo.Extensions
{ {
internal static class RoslynExtensions internal static class RoslynExtensions
{ {
public static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type) public static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type)
{ {
var current = type; var current = type;
while (current != null) while (current != null)
{ {
yield return current; yield return current;
current = current.BaseType; current = current.BaseType;
} }
} }
public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol type, bool includeBaseTypeMembers = true) public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol type, bool includeBaseTypeMembers = true)
{ {
return includeBaseTypeMembers return includeBaseTypeMembers
? type.GetBaseTypesAndThis().SelectMany(t => t.GetMembers()) ? type.GetBaseTypesAndThis().SelectMany(t => t.GetMembers())
: type.GetMembers(); : type.GetMembers();
} }
public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single(); public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single();
public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text; public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text;
public static AttributeSyntax? GetAttribute(this MemberDeclarationSyntax typeDeclarationSyntax, string attributeName) public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax typeDeclarationSyntax, string attributeName)
{ {
var attributeLists = typeDeclarationSyntax.AttributeLists; return typeDeclarationSyntax.AttributeLists
var selection = attributeLists .SelectMany(al => al.Attributes)
.SelectMany(al => al.Attributes) .SingleOrDefault(a =>
.FirstOrDefault(x => x.Name.ToString().Contains(attributeName)); (a.Name as IdentifierNameSyntax)?.Identifier.ValueText == attributeName ||
((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName);
return selection; }
}
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => symbol.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
symbol.GetAttributes().Any(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static IEnumerable<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
public static IEnumerable<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attributeSymbol) => symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
symbol.GetAttributes().Where(a => a.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) == true);
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => symbol.GetAttributes(attributeSymbol).FirstOrDefault();
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
public static string? GetNamespace(this MemberDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax .Ancestors()
.Ancestors() .OfType<NamespaceDeclarationSyntax>()
.OfType<NamespaceDeclarationSyntax>() .FirstOrDefault()
.FirstOrDefault() ?.Name
?.Name .ToString();
.ToString();
public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) =>
public static bool HasCompatibleTypes(this Compilation compilation, ISymbol source, ISymbol destination) => source.TryGetTypeSymbol(out var sourceType) && destination.TryGetTypeSymbol(out var destinationType) &&
source.TryGetTypeSymbol(out var sourceType) && destination.TryGetTypeSymbol(out var destinationType) && (SymbolEqualityComparer.Default.Equals(destinationType, sourceType) || compilation.HasImplicitConversion(sourceType, destinationType));
(SymbolEqualityComparer.Default.Equals(destinationType, sourceType) || compilation.HasImplicitConversion(sourceType, destinationType));
public static bool TryGetTypeSymbol(this ISymbol symbol, [NotNullWhen(true)] out ITypeSymbol? typeSymbol)
public static bool TryGetTypeSymbol(this ISymbol symbol, [NotNullWhen(true)] out ITypeSymbol? typeSymbol) {
{ switch (symbol)
switch (symbol) {
{ case IPropertySymbol propertySymbol:
case IPropertySymbol propertySymbol: typeSymbol = propertySymbol.Type;
typeSymbol = propertySymbol.Type; return true;
return true;
case IParameterSymbol parameterSymbol:
case IFieldSymbol fieldSymbol: typeSymbol = parameterSymbol.Type;
typeSymbol = fieldSymbol.Type; return true;
return true;
default:
case IParameterSymbol parameterSymbol: typeSymbol = null;
typeSymbol = parameterSymbol.Type; return false;
return true; }
}
default:
typeSymbol = null; public static ITypeSymbol? GetTypeSymbol(this ISymbol symbol) => symbol.TryGetTypeSymbol(out var typeSymbol) ? typeSymbol : null;
return false;
} public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty)
} {
return properties.SingleOrDefault(p =>
public static ITypeSymbol? GetTypeSymbol(this ISymbol symbol) => symbol.TryGetTypeSymbol(out var typeSymbol) ? typeSymbol : null; p.Name == targetProperty.Name &&
(p.NullableAnnotation != NullableAnnotation.Annotated ||
public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty) p.NullableAnnotation == NullableAnnotation.Annotated &&
{ targetProperty.NullableAnnotation == NullableAnnotation.Annotated));
return properties.FirstOrDefault(p => }
p.Name == targetProperty.Name &&
(p.NullableAnnotation != NullableAnnotation.Annotated || public static INamedTypeSymbol GetTypeByMetadataNameOrThrow(this Compilation compilation, string fullyQualifiedMetadataName) =>
p.NullableAnnotation == NullableAnnotation.Annotated && compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? throw new TypeLoadException($"Unable to find '{fullyQualifiedMetadataName}' type.");
targetProperty.NullableAnnotation == NullableAnnotation.Annotated));
} public static bool IsGenericEnumerable(this Compilation compilation, ITypeSymbol typeSymbol) =>
typeSymbol is INamedTypeSymbol { IsGenericType: true } &&
public static INamedTypeSymbol GetTypeByMetadataNameOrThrow(this Compilation compilation, string fullyQualifiedMetadataName) => compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).Equals(typeSymbol.OriginalDefinition, SymbolEqualityComparer.Default);
compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ?? throw new TypeLoadException($"Unable to find '{fullyQualifiedMetadataName}' type.");
public static bool IsArray(this Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol is IArrayTypeSymbol;
public static bool IsGenericEnumerable(this Compilation compilation, ITypeSymbol typeSymbol) =>
typeSymbol is INamedTypeSymbol { IsGenericType: true } && public static bool IsPrimitiveType(this ITypeSymbol type) => type.SpecialType is
compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).Equals(typeSymbol.OriginalDefinition, SymbolEqualityComparer.Default); SpecialType.System_String or
SpecialType.System_Boolean or
public static bool IsArray(this Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol is IArrayTypeSymbol; SpecialType.System_SByte or
SpecialType.System_Int16 or
public static bool IsPrimitiveType(this ITypeSymbol type) => type.SpecialType is SpecialType.System_Int32 or
SpecialType.System_String or SpecialType.System_Int64 or
SpecialType.System_Boolean or SpecialType.System_Byte or
SpecialType.System_SByte or SpecialType.System_UInt16 or
SpecialType.System_Int16 or SpecialType.System_UInt32 or
SpecialType.System_Int32 or SpecialType.System_UInt64 or
SpecialType.System_Int64 or SpecialType.System_Single or
SpecialType.System_Byte or SpecialType.System_Double or
SpecialType.System_UInt16 or SpecialType.System_Char or
SpecialType.System_UInt32 or SpecialType.System_Object;
SpecialType.System_UInt64 or
SpecialType.System_Single or public static SyntaxNode? GetSyntaxNode(this ISymbol symbol) =>
SpecialType.System_Double or symbol.Locations.FirstOrDefault() is { } location ? location.SourceTree?.GetRoot().FindNode(location.SourceSpan) : null;
SpecialType.System_Char or
SpecialType.System_Object; public static IEnumerable<INamedTypeSymbol> GetTypesByMetadataName(this Compilation compilation, string typeMetadataName)
{
public static SyntaxNode? GetSyntaxNode(this ISymbol symbol) => return compilation.References
symbol.Locations.FirstOrDefault() is { } location ? location.SourceTree?.GetRoot().FindNode(location.SourceSpan) : null; .Select(compilation.GetAssemblyOrModuleSymbol)
.OfType<IAssemblySymbol>()
public static IEnumerable<INamedTypeSymbol> GetTypesByMetadataName(this Compilation compilation, string typeMetadataName) .Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName))
{ .Where(t => t != null)!;
return compilation.References }
.Select(compilation.GetAssemblyOrModuleSymbol)
.OfType<IAssemblySymbol>() public static bool TypeByMetadataNameExists(this Compilation compilation, string typeMetadataName) => GetTypesByMetadataName(compilation, typeMetadataName).Any();
.Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(typeMetadataName)) }
.Where(t => t != null)!;
}
public static bool TypeByMetadataNameExists(this Compilation compilation, string typeMetadataName) => GetTypesByMetadataName(compilation, typeMetadataName).Any();
}
} }

View File

@ -1,30 +1,30 @@
using System; using System;
using System.Text; using System.Text;
namespace MapTo.Extensions namespace MapTo.Extensions
{ {
internal static class StringBuilderExtensions internal static class StringBuilderExtensions
{ {
public static StringBuilder PadLeft(this StringBuilder builder, int width) public static StringBuilder PadLeft(this StringBuilder builder, int width)
{ {
for (var i = 0; i < width; i++) for (var i = 0; i < width; i++)
{ {
builder.Append(" "); builder.Append(" ");
} }
return builder; return builder;
} }
internal static StringBuilder AppendOpeningBracket(this StringBuilder builder, int indent = 0) => builder.AppendLine().PadLeft(indent).AppendFormat("{{{0}", Environment.NewLine); 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) internal static StringBuilder AppendClosingBracket(this StringBuilder builder, int indent = 0, bool padNewLine = true)
{ {
if (padNewLine) if (padNewLine)
{ {
builder.AppendLine(); builder.AppendLine();
} }
return builder.PadLeft(indent).Append("}"); return builder.PadLeft(indent).Append("}");
} }
} }
} }

View File

@ -1,17 +1,17 @@
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MapTo.Extensions namespace MapTo.Extensions
{ {
internal static class StringExtensions 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 ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}";
public static string ToSourceCodeString(this object? value) => value switch public static string ToSourceCodeString(this object? value) => value switch
{ {
null => "null", null => "null",
string strValue => $"\"{strValue}\"", string strValue => $"\"{strValue}\"",
char charValue => $"'{charValue}'", char charValue => $"'{charValue}'",
_ => value.ToString() _ => value.ToString()
}; };
} }
} }

View File

@ -1,50 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Description>An object to object mapping generator using Roslyn source generator.</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <AssemblyName>MapTo</AssemblyName>
<IncludeSymbols>true</IncludeSymbols> <Description>An object to object mapping generator using Roslyn source generator.</Description>
<PackageId>MapTo</PackageId> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageProjectUrl>https://github.com/mrtaikandi/mapto</PackageProjectUrl> <IncludeSymbols>true</IncludeSymbols>
<PackageVersion>$(Version)</PackageVersion> <PackageId>MapTo</PackageId>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<RepositoryUrl>https://github.com/mrtaikandi/mapto</RepositoryUrl> <PackageProjectUrl>https://github.com/mrtaikandi/mapto</PackageProjectUrl>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RootNamespace>MapTo</RootNamespace> <PackageVersion>$(Version)</PackageVersion>
</PropertyGroup> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://github.com/mrtaikandi/mapto</RepositoryUrl>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<DocumentationFile>bin\Release\MapTo.xml</DocumentationFile> <RootNamespace>MapTo</RootNamespace>
<Optimize>false</Optimize> <AssemblyVersion>1.1</AssemblyVersion>
</PropertyGroup> <FileVersion>1.1</FileVersion>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<_Parameter1>$(AssemblyName).Tests</_Parameter1> <DocumentationFile>bin\Release\MapTo.xml</DocumentationFile>
</AssemblyAttribute> </PropertyGroup>
</ItemGroup>
<ItemGroup>
<ItemGroup> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> <_Parameter1>$(AssemblyName).Tests</_Parameter1>
<PrivateAssets>all</PrivateAssets> </AssemblyAttribute>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </ItemGroup>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" /> <ItemGroup>
<PackageReference Update="Nerdbank.GitVersioning"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
<Version>3.5.109</Version> <PrivateAssets>all</PrivateAssets>
</PackageReference> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</ItemGroup> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<ItemGroup> </ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="MapTo.props" Pack="true" PackagePath="build" Visible="false" /> <ItemGroup>
</ItemGroup> <None Include="..\..\LICENSE" Pack="true" PackagePath="" Visible="false" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<ItemGroup> <None Include="MapTo.props" Pack="true" PackagePath="build" Visible="false" />
<Folder Include="bin\Release\netstandard2.0" /> </ItemGroup>
</ItemGroup>
</Project>
</Project>

View File

@ -1,5 +1,5 @@
<Project> <Project>
<ItemGroup> <ItemGroup>
<CompilerVisibleProperty Include="MapTo_ConstructorAccessModifier" /> <CompilerVisibleProperty Include="MapTo_ConstructorAccessModifier" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,81 +1,74 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Linq;
using System.Linq; using MapTo.Extensions;
using System.Threading; using MapTo.Sources;
using MapTo.Extensions; using Microsoft.CodeAnalysis;
using MapTo.Sources; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; namespace MapTo
{
namespace MapTo /// <summary>
{ /// MapTo source generator.
/// <summary> /// </summary>
/// MapTo source generator. [Generator]
/// </summary> public class MapToGenerator : ISourceGenerator
[Generator] {
public class MapToGenerator : ISourceGenerator /// <inheritdoc />
{ public void Initialize(GeneratorInitializationContext context)
/// <inheritdoc /> {
public void Initialize(GeneratorInitializationContext context) context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver());
{ }
context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver()); /// <inheritdoc />
public void Execute(GeneratorExecutionContext context)
} {
try
/// <inheritdoc /> {
public void Execute(GeneratorExecutionContext context) var options = SourceGenerationOptions.From(context);
{
try var compilation = context.Compilation
{ .AddSource(ref context, MapFromAttributeSource.Generate(options))
var options = SourceGenerationOptions.From(context); .AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
.AddSource(ref context, ITypeConverterSource.Generate(options))
var compilation = context.Compilation .AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
.AddSource(ref context, UseUpdateAttributeSource.Generate(options)) .AddSource(ref context, MapPropertyAttributeSource.Generate(options))
.AddSource(ref context, JsonExtensionAttributeSource.Generate(options)) .AddSource(ref context, MappingContextSource.Generate(options));
.AddSource(ref context, MapFromAttributeSource.Generate(options))
.AddSource(ref context, IgnoreMemberAttributeSource.Generate(options)) if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateTypes.Any())
.AddSource(ref context, ITypeConverterSource.Generate(options)) {
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options)) AddGeneratedMappingsClasses(context, compilation, receiver.CandidateTypes, options);
.AddSource(ref context, MapPropertyAttributeSource.Generate(options)) }
.AddSource(ref context, MappingContextSource.Generate(options)); }
catch (Exception ex)
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateTypes.Any()) {
{ Console.WriteLine(ex);
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateTypes, options); throw;
} }
} }
catch (Exception ex)
{ private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<TypeDeclarationSyntax> candidateTypes, SourceGenerationOptions options)
Console.WriteLine(ex); {
throw; foreach (var typeDeclarationSyntax in candidateTypes)
} {
} var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax);
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<TypeDeclarationSyntax> candidateTypes, SourceGenerationOptions options)
{ if (mappingContext.Model is null)
foreach (var typeDeclarationSyntax in candidateTypes) {
{ continue;
var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax); }
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); var (source, hintName) = typeDeclarationSyntax switch
{
if (mappingContext.Model is null) StructDeclarationSyntax => MapStructSource.Generate(mappingContext.Model),
{ ClassDeclarationSyntax => MapClassSource.Generate(mappingContext.Model),
continue; RecordDeclarationSyntax => MapRecordSource.Generate(mappingContext.Model),
} _ => throw new ArgumentOutOfRangeException()
};
var (source, hintName) = typeDeclarationSyntax switch
{ context.AddSource(hintName, source);
StructDeclarationSyntax => MapStructSource.Generate(mappingContext.Model), }
ClassDeclarationSyntax => MapClassSource.Generate(mappingContext.Model), }
RecordDeclarationSyntax => MapRecordSource.Generate(mappingContext.Model), }
_ => throw new ArgumentOutOfRangeException()
};
context.AddSource(hintName, source);
}
}
}
} }

View File

@ -1,39 +1,39 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using MapTo.Sources; using MapTo.Sources;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo namespace MapTo
{ {
internal class MapToSyntaxReceiver : ISyntaxReceiver internal class MapToSyntaxReceiver : ISyntaxReceiver
{ {
public List<TypeDeclarationSyntax> CandidateTypes { get; } = new(); public List<TypeDeclarationSyntax> CandidateTypes { get; } = new();
/// <inheritdoc /> /// <inheritdoc />
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{ {
if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax) if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax)
{ {
return; return;
} }
var attributeSyntax = attributes var attributeSyntax = attributes
.SelectMany(a => a.Attributes) .SelectMany(a => a.Attributes)
.FirstOrDefault(a => a.Name is .SingleOrDefault(a => a.Name is
IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom] IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom]
or or
QualifiedNameSyntax // For: [MapTo.MapFrom] QualifiedNameSyntax // For: [MapTo.MapFrom]
{ {
Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } }, Left: IdentifierNameSyntax { Identifier: { ValueText: Constants.RootNamespace } },
Right: IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } Right: IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } }
} }
); );
if (attributeSyntax is not null) if (attributeSyntax is not null)
{ {
CandidateTypes.Add(typeDeclarationSyntax); CandidateTypes.Add(typeDeclarationSyntax);
} }
} }
} }
} }

340
src/MapTo/MappingContext.cs Normal file
View File

@ -0,0 +1,340 @@
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 abstract class MappingContext
{
private readonly List<SymbolDisplayPart> _ignoredNamespaces;
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
_ignoredNamespaces = new();
Diagnostics = ImmutableArray<Diagnostic>.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);
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
}
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public MappingModel? Model { get; private set; }
protected Compilation Compilation { get; }
protected INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapFromAttributeTypeSymbol { 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<string> Usings { get; private set; }
public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
MappingContext context = typeSyntax switch
{
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<IPropertySymbol> 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<MappedProperty> GetMappedProperties(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<TypeOfExpressionSyntax>()
.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 virtual MappedProperty? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<IPropertySymbol> 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<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
{
if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
{
return null;
}
}
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
return new MappedProperty(
property.Name,
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType));
}
protected bool TryGetMapTypeConverter(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.Empty;
if (!Diagnostics.IsEmpty())
{
return false;
}
var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol);
if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol)
{
return false;
}
var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null)
{
AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty));
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<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
{
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
return converterParameter.IsNull
? ImmutableArray<string>.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 shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
if (!mappedProperties.Any())
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
return null;
}
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
return new MappingModel(
SourceGenerationOptions,
TypeSyntax.GetNamespace(),
TypeSyntax.Modifiers,
TypeSyntax.Keyword.Text,
typeIdentifierName,
sourceTypeSymbol.ContainingNamespace.ToDisplayString(),
sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(),
mappedProperties,
isTypeInheritFromMappedBaseClass,
Usings,
shouldGenerateSecondaryConstructor);
}
private INamedTypeSymbol? GetTypeConverterBaseInterface(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 bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
{
var constructorSyntax = TypeSyntax.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.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;
}
}
}

84
src/MapTo/Models.cs Normal file
View File

@ -0,0 +1,84 @@
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 MappedProperty(
string Name,
string Type,
string? TypeConverter,
ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName,
string? MappedSourcePropertyTypeName,
string? EnumerableTypeArgument)
{
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,
ImmutableArray<MappedProperty> MappedProperties,
bool HasMappedBaseClass,
ImmutableArray<string> 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;
}
}

View File

@ -0,0 +1,28 @@
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<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.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()!;
}
}
}

View File

@ -1,8 +1,8 @@
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal class Constants internal class Constants
{ {
internal const string RootNamespace = "MapTo"; internal const string RootNamespace = "MapTo";
internal const string GeneratedFilesHeader = "// <auto-generated />"; internal const string GeneratedFilesHeader = "// <auto-generated />";
} }
} }

View File

@ -1,58 +1,58 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
namespace MapTo.Sources namespace MapTo.Sources
{ {
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
internal static class ITypeConverterSource internal static class ITypeConverterSource
{ {
internal const string InterfaceName = "ITypeConverter"; internal const string InterfaceName = "ITypeConverter";
internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2"; internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2";
internal static SourceCode Generate(SourceGenerationOptions options) internal static SourceCode Generate(SourceGenerationOptions options)
{ {
using var builder = new SourceBuilder() using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader) .WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine() .WriteLine()
.WriteLine($"namespace {RootNamespace}") .WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine("/// Converts the value of <typeparamref name=\"TSource\"/> to <typeparamref name=\"TDestination\"/>.") .WriteLine("/// Converts the value of <typeparamref name=\"TSource\"/> to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// </summary>") .WriteLine("/// </summary>")
.WriteLine("/// <typeparam name=\"TSource\">The type to convert from.</typeparam>") .WriteLine("/// <typeparam name=\"TSource\">The type to convert from.</typeparam>")
.WriteLine("/// <typeparam name=\"TDestination\">The type to convert to.</typeparam>"); .WriteLine("/// <typeparam name=\"TDestination\">The type to convert to.</typeparam>");
} }
builder builder
.WriteLine($"public interface {InterfaceName}<in TSource, out TDestination>") .WriteLine($"public interface {InterfaceName}<in TSource, out TDestination>")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine("/// Converts the value of <paramref name=\"source\"/> object to <typeparamref name=\"TDestination\"/>.") .WriteLine("/// Converts the value of <paramref name=\"source\"/> object to <typeparamref name=\"TDestination\"/>.")
.WriteLine("/// </summary>") .WriteLine("/// </summary>")
.WriteLine("/// <param name=\"source\">The <see cref=\"TSource\"/> to convert.</param>") .WriteLine("/// <param name=\"source\">The <see cref=\"TSource\"/> to convert.</param>")
.WriteLine($"/// <param name=\"converterParameters\">The parameter list passed to the <see cref=\"{MapTypeConverterAttributeSource.AttributeClassName}\"/></param>") .WriteLine($"/// <param name=\"converterParameters\">The parameter list passed to the <see cref=\"{MapTypeConverterAttributeSource.AttributeClassName}\"/></param>")
.WriteLine("/// <returns><typeparamref name=\"TDestination\"/> object.</returns>"); .WriteLine("/// <returns><typeparamref name=\"TDestination\"/> object.</returns>");
} }
builder builder
.WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);") .WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);")
.WriteClosingBracket() .WriteClosingBracket()
.WriteClosingBracket(); .WriteClosingBracket();
return new(builder.ToString(), $"{InterfaceName}.g.cs"); return new(builder.ToString(), $"{InterfaceName}.g.cs");
} }
internal static string GetFullyQualifiedName(ITypeSymbol sourceType, ITypeSymbol destinationType) => internal static string GetFullyQualifiedName(ITypeSymbol sourceType, ITypeSymbol destinationType) =>
$"{RootNamespace}.{InterfaceName}<{sourceType.ToDisplayString()}, {destinationType.ToDisplayString()}>"; $"{RootNamespace}.{InterfaceName}<{sourceType.ToDisplayString()}, {destinationType.ToDisplayString()}>";
} }
} }

View File

@ -1,36 +1,36 @@
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal static class ReadOnlyPropertyAttributeSource internal static class IgnorePropertyAttributeSource
{ {
internal const string AttributeName = "ReadOnlyProperty"; internal const string AttributeName = "IgnoreProperty";
internal const string AttributeClassName = AttributeName + "Attribute"; internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options) internal static SourceCode Generate(SourceGenerationOptions options)
{ {
var builder = new SourceBuilder() var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader) .WriteLine(GeneratedFilesHeader)
.WriteLine("using System;") .WriteLine("using System;")
.WriteLine() .WriteLine()
.WriteLine($"namespace {RootNamespace}") .WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated property should be excluded.") .WriteLine("/// Specifies that the annotated property should be excluded.")
.WriteLine("/// </summary>"); .WriteLine("/// </summary>");
} }
builder builder
.WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]") .WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}") .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}")
.WriteClosingBracket(); .WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs"); return new(builder.ToString(), $"{AttributeClassName}.g.cs");
} }
} }
} }

View File

@ -0,0 +1,206 @@
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapClassSource
{
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 class {model.TypeIdentifierName}")
.WriteOpeningBracket();
// Class body
if (model.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder
.GeneratePrivateConstructor(model)
.WriteLine()
.GenerateFactoryMethod(model)
.GenerateUpdateMethod(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("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
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";
var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty;
builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}")
.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);")
.WriteLine().
WriteProperties( model, sourceClassParameterName, mappingContextParameterName);
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model,
string? sourceClassParameterName, string mappingContextParameterName)
{
foreach (var property in model.MappedProperties)
{
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.WriteLine(
$"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();");
}
else
{
builder.WriteLine(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.WriteLine(
$"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});");
}
}
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 GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"public void {model.Options.NullableReferenceSyntax}Update({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteProperties( model, sourceClassParameterName,"context" )
.WriteClosingBracket();
return builder;
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>")
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>");
}
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();
}
}
}

View File

@ -1,72 +1,65 @@
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal static class MapFromAttributeSource internal static class MapFromAttributeSource
{ {
internal const string AttributeName = "MapFrom"; internal const string AttributeName = "MapFrom";
internal const string AttributeClassName = AttributeName + "Attribute"; internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options) internal static SourceCode Generate(SourceGenerationOptions options)
{ {
using var builder = new SourceBuilder() using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader) .WriteLine(GeneratedFilesHeader)
.WriteLine("using System;") .WriteLine("using System;")
.WriteLine() .WriteLine()
.WriteLine($"namespace {RootNamespace}") .WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated class can be mapped from the provided <see cref=\"SourceType\"/>.") .WriteLine("/// Specifies that the annotated class can be mapped from the provided <see cref=\"SourceType\"/>.")
.WriteLine("/// </summary>"); .WriteLine("/// </summary>");
} }
builder builder
.WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute") .WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{AttributeName}Attribute\"/> class with the specified <paramref name=\"sourceType\"/>.") .WriteLine($"/// Initializes a new instance of the <see cref=\"{AttributeName}Attribute\"/> class with the specified <paramref name=\"sourceType\"/>.")
.WriteLine("/// </summary>") .WriteLine("/// </summary>")
.WriteLine("/// <param name=\"sourceType\">The type of to map from.</param>"); .WriteLine("/// <param name=\"sourceType\">The type of to map from.</param>");
} }
builder builder
.WriteLine($"public {AttributeName}Attribute(Type sourceType)") .WriteLine($"public {AttributeName}Attribute(Type sourceType)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("SourceType = new [] { sourceType };") .WriteLine("SourceType = sourceType;")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine(); .WriteLine();
builder if (options.GenerateXmlDocument)
.WriteLine($"public {AttributeName}Attribute(Type[] sourceType)") {
.WriteOpeningBracket() builder
.WriteLine("SourceType = sourceType;") .WriteLine("/// <summary>")
.WriteClosingBracket() .WriteLine("/// Gets the type to map from.")
.WriteLine(); .WriteLine("/// </summary>");
}
if (options.GenerateXmlDocument)
{ builder
builder .WriteLine("public Type SourceType { get; }")
.WriteLine("/// <summary>") .WriteClosingBracket() // class
.WriteLine("/// Gets the type to map from.") .WriteClosingBracket(); // namespace
.WriteLine("/// </summary>");
} return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
builder }
.WriteLine("public Type[] SourceType { get; }")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
} }

View File

@ -1,61 +1,60 @@
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal static class MapPropertyAttributeSource internal static class MapPropertyAttributeSource
{ {
internal const string AttributeName = "MapProperty"; internal const string AttributeName = "MapProperty";
internal const string AttributeClassName = AttributeName + "Attribute"; internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string SourcePropertyNamePropertyName = "SourcePropertyName"; internal const string SourcePropertyNamePropertyName = "SourcePropertyName";
internal static SourceCode Generate(SourceGenerationOptions options) internal static SourceCode Generate(SourceGenerationOptions options)
{ {
using var builder = new SourceBuilder() using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader) .WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine() .WriteLine()
.WriteLine("using System;") .WriteLine("using System;")
.WriteLine() .WriteLine()
.WriteLine($"namespace {RootNamespace}") .WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine() .WriteLine("/// <summary>")
.WriteLine("/// <summary>") .WriteLine("/// Specifies the mapping behavior of the annotated property.")
.WriteLine("/// Specifies the mapping behavior of the annotated property.") .WriteLine("/// </summary>")
.WriteLine("/// </summary>") .WriteLine("/// <remarks>")
.WriteLine("/// <remarks>") .WriteLine($"/// {AttributeClassName} has a number of uses:")
.WriteLine($"/// {AttributeClassName} has a number of uses:") .WriteLine("/// <list type=\"bullet\">")
.WriteLine("/// <list type=\"bullet\">") .WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>")
.WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>") .WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>")
.WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>") .WriteLine("/// </list>")
.WriteLine("/// </list>") .WriteLine("/// </remarks>");
.WriteLine("/// </remarks>"); }
}
builder
builder .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]")
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]") .WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteLine($"public sealed class {AttributeClassName} : Attribute") .WriteOpeningBracket();
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
if (options.GenerateXmlDocument) {
{ builder
builder .WriteLine("/// <summary>")
.WriteLine("/// <summary>") .WriteLine("/// Gets or sets the property name of the object to mapping from.")
.WriteLine("/// Gets or sets the property name of the object to mapping from.") .WriteLine("/// </summary>");
.WriteLine("/// </summary>"); }
}
builder
builder .WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}")
.WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}") .WriteClosingBracket() // class
.WriteClosingBracket() // class .WriteClosingBracket(); // namespace
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
return new(builder.ToString(), $"{AttributeClassName}.g.cs"); }
} }
}
} }

View File

@ -0,0 +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("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
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.MappedProperties.Length; i++)
{
var property = model.MappedProperties[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.MappedProperties.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("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>")
.WriteLine(
$"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
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();
}
}
}

View File

@ -0,0 +1,153 @@
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapStructSource
{
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 class {model.TypeIdentifierName}")
.WriteOpeningBracket();
// Class body
if (model.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder
.GeneratePrivateConstructor(model)
.WriteLine()
// End class declaration
.WriteClosingBracket()
.WriteLine()
// 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("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
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";
var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty;
builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}")
.WriteOpeningBracket()
.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);")
.WriteLine().
WriteProperties( model, sourceClassParameterName, mappingContextParameterName);
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model,
string? sourceClassParameterName, string mappingContextParameterName)
{
foreach (var property in model.MappedProperties)
{
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.WriteLine(
$"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();");
}
else
{
builder.WriteLine(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.WriteLine(
$"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});");
}
}
return builder;
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>")
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>");
}
}
}

View File

@ -1,84 +1,84 @@
using System; using System;
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal static class MapTypeConverterAttributeSource internal static class MapTypeConverterAttributeSource
{ {
internal const string AttributeName = "MapTypeConverter"; internal const string AttributeName = "MapTypeConverter";
internal const string AttributeClassName = AttributeName + "Attribute"; internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string ConverterPropertyName = "Converter"; internal const string ConverterPropertyName = "Converter";
internal const string ConverterParametersPropertyName = "ConverterParameters"; internal const string ConverterParametersPropertyName = "ConverterParameters";
internal static SourceCode Generate(SourceGenerationOptions options) internal static SourceCode Generate(SourceGenerationOptions options)
{ {
using var builder = new SourceBuilder() using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader) .WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes) .WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine() .WriteLine()
.WriteLine("using System;") .WriteLine("using System;")
.WriteLine() .WriteLine()
.WriteLine($"namespace {RootNamespace}") .WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine("/// Specifies what type to use as a converter for the property this attribute is bound to.") .WriteLine("/// Specifies what type to use as a converter for the property this attribute is bound to.")
.WriteLine("/// </summary>"); .WriteLine("/// </summary>");
} }
builder builder
.WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]") .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute") .WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of <see cref=\"{AttributeClassName}\"/>.") .WriteLine($"/// Initializes a new instance of <see cref=\"{AttributeClassName}\"/>.")
.WriteLine("/// </summary>") .WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"converter\">The <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.</param>") .WriteLine($"/// <param name=\"converter\">The <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.</param>")
.WriteLine("/// <param name=\"converterParameters\">The list of parameters to pass to the <paramref name=\"converter\"/> during the type conversion.</param>"); .WriteLine("/// <param name=\"converterParameters\">The list of parameters to pass to the <paramref name=\"converter\"/> during the type conversion.</param>");
} }
builder builder
.WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)") .WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine($"{ConverterPropertyName} = converter;") .WriteLine($"{ConverterPropertyName} = converter;")
.WriteLine($"{ConverterParametersPropertyName} = converterParameters;") .WriteLine($"{ConverterParametersPropertyName} = converterParameters;")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine(); .WriteLine();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine($"/// Gets or sets the <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.") .WriteLine($"/// Gets or sets the <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.")
.WriteLine("/// </summary>"); .WriteLine("/// </summary>");
} }
builder builder
.WriteLine($"public Type {ConverterPropertyName} {{ get; }}") .WriteLine($"public Type {ConverterPropertyName} {{ get; }}")
.WriteLine(); .WriteLine();
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine($"/// Gets the list of parameters to pass to the <see cref=\"{ConverterPropertyName}\"/> during the type conversion.") .WriteLine($"/// Gets the list of parameters to pass to the <see cref=\"{ConverterPropertyName}\"/> during the type conversion.")
.WriteLine("/// </summary>"); .WriteLine("/// </summary>");
} }
builder builder
.WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}") .WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}")
.WriteClosingBracket() .WriteClosingBracket()
.WriteClosingBracket(); .WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs"); return new(builder.ToString(), $"{AttributeClassName}.g.cs");
} }
} }
} }

View File

@ -1,115 +1,115 @@
using System.Collections.Generic; using System.Collections.Generic;
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal static class MappingContextSource internal static class MappingContextSource
{ {
internal const string ClassName = "MappingContext"; internal const string ClassName = "MappingContext";
internal const string FullyQualifiedName = RootNamespace + "." + ClassName; internal const string FullyQualifiedName = RootNamespace + "." + ClassName;
internal const string FactoryMethodName = "Create"; internal const string FactoryMethodName = "Create";
internal const string RegisterMethodName = "Register"; internal const string RegisterMethodName = "Register";
internal const string MapMethodName = "MapFromWithContext"; internal const string MapMethodName = "MapFromWithContext";
internal static SourceCode Generate(SourceGenerationOptions options) internal static SourceCode Generate(SourceGenerationOptions options)
{ {
var usings = new List<string> { "System", "System.Collections.Generic", "System.Reflection" }; var usings = new List<string> { "System", "System.Collections.Generic", "System.Reflection" };
using var builder = new SourceBuilder() using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader) .WriteLine(GeneratedFilesHeader)
.WriteLine() .WriteLine()
.WriteUsings(usings) .WriteUsings(usings)
.WriteLine() .WriteLine()
// Namespace declaration // Namespace declaration
.WriteLine($"namespace {RootNamespace}") .WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket() .WriteOpeningBracket()
// Class declaration // Class declaration
.WriteLine($"internal sealed class {ClassName}") .WriteLine($"internal sealed class {ClassName}")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("private readonly Dictionary<object, object> _cache;") .WriteLine("private readonly Dictionary<object, object> _cache;")
.WriteLine() .WriteLine()
// Constructor // Constructor
.WriteLine($"internal {ClassName}()") .WriteLine($"internal {ClassName}()")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("_cache = new Dictionary<object, object>(1);") .WriteLine("_cache = new Dictionary<object, object>(1);")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
// Factory // Factory
.WriteLine($"internal static TMapped {FactoryMethodName}<TOriginal, TMapped>(TOriginal original)") .WriteLine($"internal static TMapped {FactoryMethodName}<TOriginal, TMapped>(TOriginal original)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));") .WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
.WriteLine() .WriteLine()
.WriteLine("var context = new MappingContext();") .WriteLine("var context = new MappingContext();")
.WriteLine("var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);") .WriteLine("var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);")
.WriteLine() .WriteLine()
.WriteLine("if (mapped == null)") .WriteLine("if (mapped == null)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("throw new InvalidOperationException();") .WriteLine("throw new InvalidOperationException();")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
.WriteLine("return mapped;") .WriteLine("return mapped;")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
// MapFromWithContext method // MapFromWithContext method
.WriteLine($"internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)") .WriteLine($"internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("if (original == null)") .WriteLine("if (original == null)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("return default(TMapped);") .WriteLine("return default(TMapped);")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
.WriteLine("if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))") .WriteLine("if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);") .WriteLine("var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);")
.WriteLine("if (instance != null)") .WriteLine("if (instance != null)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("mapped = (TMapped)instance;") .WriteLine("mapped = (TMapped)instance;")
.WriteClosingBracket() .WriteClosingBracket()
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
.WriteLine("return mapped;") .WriteLine("return mapped;")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
// Register method // Register method
.WriteLine("internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)") .WriteLine("internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));") .WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
.WriteLine("if (mapped == null) throw new ArgumentNullException(nameof(mapped));") .WriteLine("if (mapped == null) throw new ArgumentNullException(nameof(mapped));")
.WriteLine() .WriteLine()
.WriteLine("if (!_cache.ContainsKey(original))") .WriteLine("if (!_cache.ContainsKey(original))")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("_cache.Add(original, mapped);") .WriteLine("_cache.Add(original, mapped);")
.WriteClosingBracket() .WriteClosingBracket()
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
// TryGetValue method // TryGetValue method
.WriteLine("private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)") .WriteLine("private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("if (original != null && _cache.TryGetValue(original, out var value))") .WriteLine("if (original != null && _cache.TryGetValue(original, out var value))")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("mapped = (TMapped)value;") .WriteLine("mapped = (TMapped)value;")
.WriteLine("return true;") .WriteLine("return true;")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
.WriteLine("mapped = default(TMapped);") .WriteLine("mapped = default(TMapped);")
.WriteLine("return false;") .WriteLine("return false;")
.WriteClosingBracket() .WriteClosingBracket()
// End class declaration // End class declaration
.WriteClosingBracket() .WriteClosingBracket()
// End namespace declaration // End namespace declaration
.WriteClosingBracket(); .WriteClosingBracket();
return new(builder.ToString(), $"{ClassName}.g.cs"); return new(builder.ToString(), $"{ClassName}.g.cs");
} }
} }
} }

View File

@ -1,107 +1,107 @@
using System; using System;
using System.CodeDom.Compiler; using System.CodeDom.Compiler;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal sealed class SourceBuilder : IDisposable internal sealed class SourceBuilder : IDisposable
{ {
private readonly StringWriter _writer; private readonly StringWriter _writer;
private readonly IndentedTextWriter _indentedWriter; private readonly IndentedTextWriter _indentedWriter;
public SourceBuilder() public SourceBuilder()
{ {
_writer = new StringWriter(); _writer = new StringWriter();
_indentedWriter = new IndentedTextWriter(_writer, new string(' ', 4)); _indentedWriter = new IndentedTextWriter(_writer, new string(' ', 4));
} }
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
_writer.Dispose(); _writer.Dispose();
_indentedWriter.Dispose(); _indentedWriter.Dispose();
} }
public SourceBuilder WriteLine(string? value = null) public SourceBuilder WriteLine(string? value = null)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
_indentedWriter.WriteLineNoTabs(string.Empty); _indentedWriter.WriteLineNoTabs(string.Empty);
} }
else else
{ {
_indentedWriter.WriteLine(value); _indentedWriter.WriteLine(value);
} }
return this; return this;
} }
public SourceBuilder Write(string? value = null) public SourceBuilder Write(string? value = null)
{ {
_indentedWriter.Write(value); _indentedWriter.Write(value);
return this; return this;
} }
public SourceBuilder WriteLineIf(bool condition, string? value) public SourceBuilder WriteLineIf(bool condition, string? value)
{ {
if (condition) if (condition)
{ {
WriteLine(value); WriteLine(value);
} }
return this; return this;
} }
public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable"); public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable");
public SourceBuilder WriteOpeningBracket() public SourceBuilder WriteOpeningBracket()
{ {
_indentedWriter.WriteLine("{"); _indentedWriter.WriteLine("{");
_indentedWriter.Indent++; _indentedWriter.Indent++;
return this; return this;
} }
public SourceBuilder WriteClosingBracket() public SourceBuilder WriteClosingBracket()
{ {
_indentedWriter.Indent--; _indentedWriter.Indent--;
_indentedWriter.WriteLine("}"); _indentedWriter.WriteLine("}");
return this; return this;
} }
public SourceBuilder WriteUsings(IEnumerable<string> usings) public SourceBuilder WriteUsings(IEnumerable<string> usings)
{ {
foreach (var u in usings.OrderBy(s => s)) foreach (var u in usings.OrderBy(s => s))
{ {
WriteUsing(u); WriteUsing(u);
} }
return this; return this;
} }
public SourceBuilder WriteUsing(string u) public SourceBuilder WriteUsing(string u)
{ {
WriteLine($"using {u};"); WriteLine($"using {u};");
return this; return this;
} }
public SourceBuilder Indent() public SourceBuilder Indent()
{ {
_indentedWriter.Indent++; _indentedWriter.Indent++;
return this; return this;
} }
public SourceBuilder Unindent() public SourceBuilder Unindent()
{ {
_indentedWriter.Indent--; _indentedWriter.Indent--;
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => _writer.ToString(); public override string ToString() => _writer.ToString();
} }
} }

View File

@ -1,85 +1,85 @@
using System.Linq; using System.Linq;
using MapTo.Integration.Tests.Data.Models; using MapTo.Integration.Tests.Data.Models;
using MapTo.Integration.Tests.Data.ViewModels; using MapTo.Integration.Tests.Data.ViewModels;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace MapTo.Integration.Tests namespace MapTo.Integration.Tests
{ {
public class CyclicReferenceTests public class CyclicReferenceTests
{ {
[Fact] [Fact]
public void VerifySelfReference() public void VerifySelfReference()
{ {
// Arrange // Arrange
var manager = new Manager { Id = 1, EmployeeCode = "M001", Level = 100 }; var manager = new Manager { Id = 1, EmployeeCode = "M001", Level = 100 };
manager.Manager = manager; manager.Manager = manager;
// Act // Act
var result = manager.ToManagerViewModel(); var result = manager.ToManagerViewModel();
// Assert // Assert
result.Id.ShouldBe(manager.Id); result.Id.ShouldBe(manager.Id);
result.EmployeeCode.ShouldBe(manager.EmployeeCode); result.EmployeeCode.ShouldBe(manager.EmployeeCode);
result.Level.ShouldBe(manager.Level); result.Level.ShouldBe(manager.Level);
result.Manager.ShouldBeSameAs(result); result.Manager.ShouldBeSameAs(result);
} }
[Fact] [Fact]
public void VerifyNestedReference() public void VerifyNestedReference()
{ {
// Arrange // Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 }; var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 }; var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"}; var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"}; var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
employee1.Manager = manager1; employee1.Manager = manager1;
employee2.Manager = manager2; employee2.Manager = manager2;
manager2.Manager = manager1; manager2.Manager = manager1;
// Act // Act
var manager1ViewModel = manager1.ToManagerViewModel(); var manager1ViewModel = manager1.ToManagerViewModel();
// Assert // Assert
manager1ViewModel.Id.ShouldBe(manager1.Id); manager1ViewModel.Id.ShouldBe(manager1.Id);
manager1ViewModel.Manager.ShouldBeNull(); manager1ViewModel.Manager.ShouldBeNull();
manager1ViewModel.Employees.Count.ShouldBe(2); manager1ViewModel.Employees.Count.ShouldBe(2);
manager1ViewModel.Employees[0].Id.ShouldBe(employee1.Id); manager1ViewModel.Employees[0].Id.ShouldBe(employee1.Id);
manager1ViewModel.Employees[0].Manager.ShouldBeSameAs(manager1ViewModel); manager1ViewModel.Employees[0].Manager.ShouldBeSameAs(manager1ViewModel);
manager1ViewModel.Employees[1].Id.ShouldBe(manager2.Id); manager1ViewModel.Employees[1].Id.ShouldBe(manager2.Id);
manager1ViewModel.Employees[1].Manager.ShouldBeSameAs(manager1ViewModel); manager1ViewModel.Employees[1].Manager.ShouldBeSameAs(manager1ViewModel);
} }
[Fact] [Fact]
public void VerifyNestedSelfReference() public void VerifyNestedSelfReference()
{ {
// Arrange // Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 }; var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager3 = new Manager { Id = 101, EmployeeCode = "M003", Level = 100 }; var manager3 = new Manager { Id = 101, EmployeeCode = "M003", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 }; var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"}; var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"}; var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
var employee3 = new Employee { Id = 202, EmployeeCode = "E003"}; var employee3 = new Employee { Id = 202, EmployeeCode = "E003"};
employee1.Manager = manager1; employee1.Manager = manager1;
employee2.Manager = manager2; employee2.Manager = manager2;
employee3.Manager = manager3; employee3.Manager = manager3;
manager2.Manager = manager1; manager2.Manager = manager1;
manager3.Manager = manager2; manager3.Manager = manager2;
// Act // Act
var manager3ViewModel = manager3.ToManagerViewModel(); var manager3ViewModel = manager3.ToManagerViewModel();
// Assert // Assert
manager3ViewModel.Manager.ShouldNotBeNull(); manager3ViewModel.Manager.ShouldNotBeNull();
manager3ViewModel.Manager.Id.ShouldBe(manager2.Id); manager3ViewModel.Manager.Id.ShouldBe(manager2.Id);
manager3ViewModel.Manager.Manager.Id.ShouldBe(manager1.Id); manager3ViewModel.Manager.Manager.Id.ShouldBe(manager1.Id);
manager3ViewModel.Employees.All(e => ReferenceEquals(e.Manager, manager3ViewModel)).ShouldBeTrue(); manager3ViewModel.Employees.All(e => ReferenceEquals(e.Manager, manager3ViewModel)).ShouldBeTrue();
} }
} }
} }

View File

@ -1,29 +1,29 @@
namespace MapTo.Integration.Tests.Data.Models namespace MapTo.Integration.Tests.Data.Models
{ {
public class Employee public class Employee
{ {
private Manager _manager; private Manager _manager;
public int Id { get; set; } public int Id { get; set; }
public string EmployeeCode { get; set; } public string EmployeeCode { get; set; }
public Manager Manager public Manager Manager
{ {
get => _manager; get => _manager;
set set
{ {
if (value == null) if (value == null)
{ {
_manager.Employees.Remove(this); _manager.Employees.Remove(this);
} }
else else
{ {
value.Employees.Add(this); value.Employees.Add(this);
} }
_manager = value; _manager = value;
} }
} }
} }
} }

View File

@ -1,11 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace MapTo.Integration.Tests.Data.Models namespace MapTo.Integration.Tests.Data.Models
{ {
public class Manager : Employee public class Manager : Employee
{ {
public int Level { get; set; } public int Level { get; set; }
public List<Employee> Employees { get; set; } = new(); public List<Employee> Employees { get; set; } = new();
} }
} }

View File

@ -1,14 +1,14 @@
using MapTo.Integration.Tests.Data.Models; using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels namespace MapTo.Integration.Tests.Data.ViewModels
{ {
[MapFrom(typeof(Employee))] [MapFrom(typeof(Employee))]
public partial class EmployeeViewModel public partial class EmployeeViewModel
{ {
public int Id { get; set; } public int Id { get; set; }
public string EmployeeCode { get; set; } public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; } public ManagerViewModel Manager { get; set; }
} }
} }

View File

@ -1,13 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using MapTo.Integration.Tests.Data.Models; using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels namespace MapTo.Integration.Tests.Data.ViewModels
{ {
[MapFrom(typeof(Manager))] [MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel public partial class ManagerViewModel : EmployeeViewModel
{ {
public int Level { get; set; } public int Level { get; set; }
public List<EmployeeViewModel> Employees { get; set; } = new(); public List<EmployeeViewModel> Employees { get; set; } = new();
} }
} }

View File

@ -1,26 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> <ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Shouldly" Version="4.0.3" /> <PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3"> <PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,252 +1,252 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using MapTo.Extensions; using MapTo.Extensions;
using MapTo.Sources; using MapTo.Sources;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly; using Shouldly;
namespace MapTo.Tests namespace MapTo.Tests
{ {
internal static class Common internal static class Common
{ {
internal const int Indent1 = 4; internal const int Indent1 = 4;
internal const int Indent2 = Indent1 * 2; internal const int Indent2 = Indent1 * 2;
internal const int Indent3 = Indent1 * 3; internal const int Indent3 = Indent1 * 3;
internal static readonly Location IgnoreLocation = Location.None; internal static readonly Location IgnoreLocation = Location.None;
internal static readonly Dictionary<string, string> DefaultAnalyzerOptions = new() internal static readonly Dictionary<string, string> DefaultAnalyzerOptions = new()
{ {
[GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false" [GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false"
}; };
internal static string GetSourceText(SourceGeneratorOptions? options = null) internal static string GetSourceText(SourceGeneratorOptions? options = null)
{ {
const string ns = "Test"; const string ns = "Test";
options ??= new SourceGeneratorOptions(); options ??= new SourceGeneratorOptions();
var hasDifferentSourceNamespace = options.SourceClassNamespace != ns; var hasDifferentSourceNamespace = options.SourceClassNamespace != ns;
var builder = new SourceBuilder(); var builder = new SourceBuilder();
builder.WriteLine("//"); builder.WriteLine("//");
builder.WriteLine("// Test source code."); builder.WriteLine("// Test source code.");
builder.WriteLine("//"); builder.WriteLine("//");
builder.WriteLine(); builder.WriteLine();
options.Usings?.ForEach(s => builder.WriteLine($"using {s};")); options.Usings?.ForEach(s => builder.WriteLine($"using {s};"));
if (options.UseMapToNamespace) if (options.UseMapToNamespace)
{ {
builder.WriteLine($"using {Constants.RootNamespace};"); builder.WriteLine($"using {Constants.RootNamespace};");
} }
builder builder
.WriteLine($"using {options.SourceClassNamespace};") .WriteLine($"using {options.SourceClassNamespace};")
.WriteLine() .WriteLine()
.WriteLine(); .WriteLine();
builder builder
.WriteLine($"namespace {ns}") .WriteLine($"namespace {ns}")
.WriteOpeningBracket(); .WriteOpeningBracket();
if (hasDifferentSourceNamespace && options.UseMapToNamespace) if (hasDifferentSourceNamespace && options.UseMapToNamespace)
{ {
builder builder
.WriteLine($"using {options.SourceClassNamespace};") .WriteLine($"using {options.SourceClassNamespace};")
.WriteLine() .WriteLine()
.WriteLine(); .WriteLine();
} }
builder builder
.WriteLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]") .WriteLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]")
.WriteLine("public partial class Foo") .WriteLine("public partial class Foo")
.WriteOpeningBracket(); .WriteOpeningBracket();
for (var i = 1; i <= options.ClassPropertiesCount; i++) for (var i = 1; i <= options.ClassPropertiesCount; i++)
{ {
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
} }
options.PropertyBuilder?.Invoke(builder); options.PropertyBuilder?.Invoke(builder);
builder builder
.WriteClosingBracket() .WriteClosingBracket()
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
.WriteLine(); .WriteLine();
builder builder
.WriteLine($"namespace {options.SourceClassNamespace}") .WriteLine($"namespace {options.SourceClassNamespace}")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("public class Baz") .WriteLine("public class Baz")
.WriteOpeningBracket(); .WriteOpeningBracket();
for (var i = 1; i <= options.SourceClassPropertiesCount; i++) for (var i = 1; i <= options.SourceClassPropertiesCount; i++)
{ {
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}"); builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
} }
options.SourcePropertyBuilder?.Invoke(builder); options.SourcePropertyBuilder?.Invoke(builder);
builder builder
.WriteClosingBracket() .WriteClosingBracket()
.WriteClosingBracket(); .WriteClosingBracket();
return builder.ToString(); return builder.ToString();
} }
internal static string[] GetEmployeeManagerSourceText( internal static string[] GetEmployeeManagerSourceText(
Func<string>? employeeClassSource = null, Func<string>? employeeClassSource = null,
Func<string>? managerClassSource = null, Func<string>? managerClassSource = null,
Func<string>? employeeViewModelSource = null, Func<string>? employeeViewModelSource = null,
Func<string>? managerViewModelSource = null, Func<string>? managerViewModelSource = null,
bool useDifferentViewModelNamespace = false) bool useDifferentViewModelNamespace = false)
{ {
return new[] return new[]
{ {
employeeClassSource?.Invoke() ?? DefaultEmployeeClassSource(), employeeClassSource?.Invoke() ?? DefaultEmployeeClassSource(),
managerClassSource?.Invoke() ?? DefaultManagerClassSource(), managerClassSource?.Invoke() ?? DefaultManagerClassSource(),
employeeViewModelSource?.Invoke() ?? employeeViewModelSource?.Invoke() ??
DefaultEmployeeViewModelSource(useDifferentViewModelNamespace), DefaultEmployeeViewModelSource(useDifferentViewModelNamespace),
managerViewModelSource?.Invoke() ?? DefaultManagerViewModelSource(useDifferentViewModelNamespace) managerViewModelSource?.Invoke() ?? DefaultManagerViewModelSource(useDifferentViewModelNamespace)
}; };
static string DefaultEmployeeClassSource() => static string DefaultEmployeeClassSource() =>
@" @"
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace Test.Data.Models namespace Test.Data.Models
{ {
public class Employee public class Employee
{ {
public int Id { get; set; } public int Id { get; set; }
public string EmployeeCode { get; set; } public string EmployeeCode { get; set; }
public Manager Manager { get; set; } public Manager Manager { get; set; }
} }
}".Trim(); }".Trim();
static string DefaultManagerClassSource() => static string DefaultManagerClassSource() =>
@"using System; @"using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace Test.Data.Models namespace Test.Data.Models
{ {
public class Manager: Employee public class Manager: Employee
{ {
public int Level { get; set; } public int Level { get; set; }
public IEnumerable<Employee> Employees { get; set; } = Array.Empty<Employee>(); public IEnumerable<Employee> Employees { get; set; } = Array.Empty<Employee>();
} }
} }
".Trim(); ".Trim();
static string DefaultEmployeeViewModelSource(bool useDifferentNamespace) => useDifferentNamespace static string DefaultEmployeeViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @" ? @"
using MapTo; using MapTo;
using Test.Data.Models; using Test.Data.Models;
using Test.ViewModels2; using Test.ViewModels2;
namespace Test.ViewModels namespace Test.ViewModels
{ {
[MapFrom(typeof(Employee))] [MapFrom(typeof(Employee))]
public partial class EmployeeViewModel public partial class EmployeeViewModel
{ {
public int Id { get; set; } public int Id { get; set; }
public string EmployeeCode { get; set; } public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; } public ManagerViewModel Manager { get; set; }
} }
} }
".Trim() ".Trim()
: @" : @"
using MapTo; using MapTo;
using Test.Data.Models; using Test.Data.Models;
namespace Test.ViewModels namespace Test.ViewModels
{ {
[MapFrom(typeof(Employee))] [MapFrom(typeof(Employee))]
public partial class EmployeeViewModel public partial class EmployeeViewModel
{ {
public int Id { get; set; } public int Id { get; set; }
public string EmployeeCode { get; set; } public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; } public ManagerViewModel Manager { get; set; }
} }
} }
".Trim(); ".Trim();
static string DefaultManagerViewModelSource(bool useDifferentNamespace) => useDifferentNamespace static string DefaultManagerViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @" ? @"
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MapTo; using MapTo;
using Test.Data.Models; using Test.Data.Models;
using Test.ViewModels; using Test.ViewModels;
namespace Test.ViewModels2 namespace Test.ViewModels2
{ {
[MapFrom(typeof(Manager))] [MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel public partial class ManagerViewModel : EmployeeViewModel
{ {
public int Level { get; set; } public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>(); public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
} }
} }
".Trim() ".Trim()
: @" : @"
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using MapTo; using MapTo;
using Test.Data.Models; using Test.Data.Models;
namespace Test.ViewModels namespace Test.ViewModels
{ {
[MapFrom(typeof(Manager))] [MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel public partial class ManagerViewModel : EmployeeViewModel
{ {
public int Level { get; set; } public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>(); public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
} }
}".Trim(); }".Trim();
} }
internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo") internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
{ {
return syntaxTree.GetRoot() return syntaxTree.GetRoot()
.DescendantNodes() .DescendantNodes()
.OfType<ClassDeclarationSyntax>() .OfType<ClassDeclarationSyntax>()
.Single(c => c.Identifier.ValueText == targetClass) .Single(c => c.Identifier.ValueText == targetClass)
.DescendantNodes() .DescendantNodes()
.OfType<PropertyDeclarationSyntax>() .OfType<PropertyDeclarationSyntax>()
.Single(p => p.Identifier.ValueText == targetPropertyName); .Single(p => p.Identifier.ValueText == targetPropertyName);
} }
internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo") internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo")
{ {
var syntaxTree = compilation.SyntaxTrees.First(); var syntaxTree = compilation.SyntaxTrees.First();
var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass); var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass);
var semanticModel = compilation.GetSemanticModel(syntaxTree); var semanticModel = compilation.GetSemanticModel(syntaxTree);
return semanticModel.GetDeclaredSymbol(propSyntax).ShouldNotBeNull(); return semanticModel.GetDeclaredSymbol(propSyntax).ShouldNotBeNull();
} }
internal record SourceGeneratorOptions( internal record SourceGeneratorOptions(
bool UseMapToNamespace = false, bool UseMapToNamespace = false,
string SourceClassNamespace = "Test.Models", string SourceClassNamespace = "Test.Models",
int ClassPropertiesCount = 3, int ClassPropertiesCount = 3,
int SourceClassPropertiesCount = 3, int SourceClassPropertiesCount = 3,
Action<SourceBuilder>? PropertyBuilder = null, Action<SourceBuilder>? PropertyBuilder = null,
Action<SourceBuilder>? SourcePropertyBuilder = null, Action<SourceBuilder>? SourcePropertyBuilder = null,
IEnumerable<string>? Usings = null); IEnumerable<string>? Usings = null);
} }
} }

View File

@ -1,16 +1,16 @@
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
// ReSharper disable CheckNamespace // ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements. // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel; using System.ComponentModel;
namespace System.Runtime.CompilerServices namespace System.Runtime.CompilerServices
{ {
/// <summary> /// <summary>
/// Reserved to be used by the compiler for tracking metadata. /// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code. /// This class should not be used by developers in source code.
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { } internal static class IsExternalInit { }
} }

View File

@ -1,42 +1,42 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace MapTo.Tests.Extensions namespace MapTo.Tests.Extensions
{ {
internal static class RoslynExtensions internal static class RoslynExtensions
{ {
internal static SyntaxTree? GetGeneratedSyntaxTree(this Compilation compilation, string className) => internal static SyntaxTree? GetGeneratedSyntaxTree(this Compilation compilation, string className) =>
compilation.SyntaxTrees.FirstOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs")); compilation.SyntaxTrees.SingleOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs"));
internal static string PrintSyntaxTree(this Compilation compilation) internal static string PrintSyntaxTree(this Compilation compilation)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
return string.Join( return string.Join(
Environment.NewLine, Environment.NewLine,
compilation.SyntaxTrees compilation.SyntaxTrees
.Reverse() .Reverse()
.Select((s, i) => .Select((s, i) =>
{ {
builder builder
.Clear() .Clear()
.AppendLine("----------------------------------------") .AppendLine("----------------------------------------")
.AppendFormat("File Path: \"{0}\"", s.FilePath).AppendLine() .AppendFormat("File Path: \"{0}\"", s.FilePath).AppendLine()
.AppendFormat("Index: \"{0}\"", i).AppendLine() .AppendFormat("Index: \"{0}\"", i).AppendLine()
.AppendLine(); .AppendLine();
var lines = s.ToString().Split(Environment.NewLine); var lines = s.ToString().Split(Environment.NewLine);
var lineNumber = 0; var lineNumber = 0;
foreach (var line in lines) foreach (var line in lines)
{ {
builder.AppendFormat("{0:00}: {1}", lineNumber, line).AppendLine(); builder.AppendFormat("{0:00}: {1}", lineNumber, line).AppendLine();
lineNumber++; lineNumber++;
} }
return builder.ToString(); return builder.ToString();
})); }));
} }
} }
} }

View File

@ -1,88 +1,88 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace MapTo.Tests.Extensions namespace MapTo.Tests.Extensions
{ {
internal static class ShouldlyExtensions internal static class ShouldlyExtensions
{ {
internal static void ShouldContainSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null) internal static void ShouldContainSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{ {
var syntax = syntaxTree var syntax = syntaxTree
.Select(s => s.ToString().Trim()) .Select(s => s.ToString().Trim())
.FirstOrDefault(s => s.Contains(typeName)); .SingleOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace(); syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldBe(expectedSource, customMessage); syntax.ShouldBe(expectedSource, customMessage);
} }
internal static void ShouldContainPartialSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null) internal static void ShouldContainPartialSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{ {
var syntax = syntaxTree var syntax = syntaxTree
.Select(s => s.ToString().Trim()) .Select(s => s.ToString().Trim())
.FirstOrDefault(s => s.Contains(typeName)); .SingleOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace(); syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage); syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
} }
internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string? customMessage = null) internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string? customMessage = null)
{ {
var syntax = syntaxTree.ToString(); var syntax = syntaxTree.ToString();
syntax.ShouldNotBeNullOrWhiteSpace(); syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage); syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
} }
internal static void ShouldBeSuccessful(this IEnumerable<Diagnostic> diagnostics, Compilation? compilation = null, IEnumerable<string>? ignoreDiagnosticsIds = null) internal static void ShouldBeSuccessful(this IEnumerable<Diagnostic> diagnostics, Compilation? compilation = null, IEnumerable<string>? ignoreDiagnosticsIds = null)
{ {
var actual = diagnostics var actual = diagnostics
.Where(d => (ignoreDiagnosticsIds is null || ignoreDiagnosticsIds.All(i => !d.Id.StartsWith(i) )) && (d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error)) .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(); .Select(c => $"{c.Severity}: {c.Location.GetLineSpan()} - {c.GetMessage()}").ToArray();
if (!actual.Any()) if (!actual.Any())
{ {
return; return;
} }
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("Failed"); builder.AppendLine("Failed");
foreach (var d in actual) foreach (var d in actual)
{ {
builder.AppendFormat("- {0}", d).AppendLine(); builder.AppendFormat("- {0}", d).AppendLine();
} }
if (compilation is not null) if (compilation is not null)
{ {
builder.AppendLine("Generated Sources:"); builder.AppendLine("Generated Sources:");
builder.AppendLine(compilation.PrintSyntaxTree()); builder.AppendLine(compilation.PrintSyntaxTree());
} }
Assert.False(true, builder.ToString()); Assert.False(true, builder.ToString());
} }
internal static void ShouldNotBeSuccessful(this ImmutableArray<Diagnostic> diagnostics, Diagnostic expectedError) internal static void ShouldNotBeSuccessful(this ImmutableArray<Diagnostic> diagnostics, Diagnostic expectedError)
{ {
var actualDiagnostics = diagnostics.FirstOrDefault(d => d.Id == expectedError.Id); var actualDiagnostics = diagnostics.SingleOrDefault(d => d.Id == expectedError.Id);
var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics }); var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics });
compilationDiagnostics.ShouldBeSuccessful(); compilationDiagnostics.ShouldBeSuccessful();
Assert.NotNull(actualDiagnostics); Assert.NotNull(actualDiagnostics);
Assert.Equal(expectedError.Id, actualDiagnostics?.Id); Assert.Equal(expectedError.Id, actualDiagnostics?.Id);
Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics?.Descriptor.Id); Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics?.Descriptor.Id);
Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics?.Descriptor.Description); Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics?.Descriptor.Description);
Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics?.Descriptor.Title); Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics?.Descriptor.Title);
if (expectedError.Location != Location.None) if (expectedError.Location != Location.None)
{ {
Assert.Equal(expectedError.Location, actualDiagnostics?.Location); Assert.Equal(expectedError.Location, actualDiagnostics?.Location);
} }
} }
} }
} }

View File

@ -1,81 +1,79 @@
using System.Linq; using System.Linq;
using MapTo.Extensions; using MapTo.Extensions;
using MapTo.Sources; using MapTo.Sources;
using MapTo.Tests.Extensions; using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure; using MapTo.Tests.Infrastructure;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
using static MapTo.Tests.Common; using static MapTo.Tests.Common;
namespace MapTo.Tests namespace MapTo.Tests
{ {
public class IgnorePropertyAttributeTests public class IgnorePropertyAttributeTests
{ {
/* [Fact]
[Fact] public void VerifyIgnorePropertyAttribute()
public void VerifyIgnorePropertyAttribute() {
{ // Arrange
// Arrange const string source = "";
const string source = ""; var expectedAttribute = $@"
var expectedAttribute = $@" {Constants.GeneratedFilesHeader}
{Constants.GeneratedFilesHeader} using System;
using System;
namespace MapTo
namespace MapTo {{
{{ [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class IgnorePropertyAttribute : Attribute {{ }}
public sealed class IgnorePropertyAttribute : Attribute {{ }} }}
}} ".Trim();
".Trim();
// Act
// Act var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
// Assert diagnostics.ShouldBeSuccessful();
diagnostics.ShouldBeSuccessful(); compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute);
compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute); }
}
*/ [Fact]
public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty()
[Fact] {
public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty() // Arrange
{ var source = GetSourceText(new SourceGeneratorOptions(
// Arrange true,
var source = GetSourceText(new SourceGeneratorOptions( PropertyBuilder: builder =>
true, {
PropertyBuilder: builder => builder
{ .WriteLine("[IgnoreProperty]")
builder .WriteLine("public int Prop4 { get; set; }");
.WriteLine("[IgnoreProperty]") },
.WriteLine("public int Prop4 { get; set; }"); SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
},
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); var expectedResult = @"
partial class Foo
var expectedResult = @" {
partial class Foo public Foo(Test.Models.Baz baz)
{ : this(new MappingContext(), baz) { }
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { } private protected Foo(MappingContext context, Test.Models.Baz 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));
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz)); context.Register(baz, this);
context.Register(baz, this); Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop1 = baz.Prop1; Prop3 = baz.Prop3;
Prop2 = baz.Prop2; }
Prop3 = baz.Prop3; ".Trim();
}
".Trim(); // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); // Assert
diagnostics.ShouldBeSuccessful();
// Assert compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
diagnostics.ShouldBeSuccessful(); }
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); }
}
}
} }

View File

@ -1,66 +1,64 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Linq;
using System.Linq; using MapTo.Tests.Extensions;
using System.Threading; using Microsoft.CodeAnalysis;
using MapTo.Tests.Extensions; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; namespace MapTo.Tests.Infrastructure
{
namespace MapTo.Tests.Infrastructure internal static class CSharpGenerator
{ {
internal static class CSharpGenerator internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
{ string source,
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation( bool assertCompilation = false,
string source, IDictionary<string, string>? analyzerConfigOptions = null,
bool assertCompilation = false, NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
IDictionary<string, string>? analyzerConfigOptions = null, LanguageVersion languageVersion = LanguageVersion.CSharp7_3) =>
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable, GetOutputCompilation(
LanguageVersion languageVersion = LanguageVersion.CSharp7_3) => new[] { source },
GetOutputCompilation( assertCompilation,
new[] { source }, analyzerConfigOptions,
assertCompilation, nullableContextOptions,
analyzerConfigOptions, languageVersion);
nullableContextOptions,
languageVersion); internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
IEnumerable<string> sources,
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation( bool assertCompilation = false,
IEnumerable<string> sources, IDictionary<string, string>? analyzerConfigOptions = null,
bool assertCompilation = false, NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
IDictionary<string, string>? analyzerConfigOptions = null, LanguageVersion languageVersion = LanguageVersion.CSharp7_3)
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable, {
LanguageVersion languageVersion = LanguageVersion.CSharp7_3) var references = AppDomain.CurrentDomain.GetAssemblies()
{ .Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location))
var references = AppDomain.CurrentDomain.GetAssemblies() .Select(a => MetadataReference.CreateFromFile(a.Location))
.Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location)) .ToList();
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToList(); var compilation = CSharpCompilation.Create(
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic",
var compilation = CSharpCompilation.Create( sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs", options: new CSharpParseOptions(languageVersion))),
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", references,
sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs", options: new CSharpParseOptions(languageVersion))), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions));
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions)); if (assertCompilation)
{
if (assertCompilation) // NB: fail tests when the injected program isn't valid _before_ running generators
{ compilation.GetDiagnostics().ShouldBeSuccessful();
// NB: fail tests when the injected program isn't valid _before_ running generators }
compilation.GetDiagnostics().ShouldBeSuccessful();
} var driver = CSharpGeneratorDriver.Create(
new[] { new MapToGenerator() },
var driver = CSharpGeneratorDriver.Create( optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions),
new List<ISourceGenerator>() { new MapToGenerator()}, parseOptions: new CSharpParseOptions(languageVersion)
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions), );
parseOptions: new CSharpParseOptions(languageVersion)
); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics); generateDiagnostics.ShouldBeSuccessful(ignoreDiagnosticsIds: new[] { "MT" });
outputCompilation.GetDiagnostics().ShouldBeSuccessful(outputCompilation);
generateDiagnostics.ShouldBeSuccessful(ignoreDiagnosticsIds: new[] { "MT" });
outputCompilation.GetDiagnostics().ShouldBeSuccessful(outputCompilation); return (outputCompilation, generateDiagnostics);
}
return (outputCompilation, generateDiagnostics); }
}
}
} }

View File

@ -1,19 +1,19 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure namespace MapTo.Tests.Infrastructure
{ {
internal sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions internal sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions
{ {
private readonly ImmutableDictionary<string, string> _backing; private readonly ImmutableDictionary<string, string> _backing;
public TestAnalyzerConfigOptions(IDictionary<string, string>? properties) public TestAnalyzerConfigOptions(IDictionary<string, string>? properties)
{ {
_backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create<string, string>(KeyComparer); _backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create<string, string>(KeyComparer);
} }
public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value); public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value);
} }
} }

View File

@ -1,24 +1,24 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure namespace MapTo.Tests.Infrastructure
{ {
internal sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider internal sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{ {
public TestAnalyzerConfigOptionsProvider(IDictionary<string, string>? options) public TestAnalyzerConfigOptionsProvider(IDictionary<string, string>? options)
{ {
GlobalOptions = new TestAnalyzerConfigOptions(options); GlobalOptions = new TestAnalyzerConfigOptions(options);
} }
/// <inheritdoc /> /// <inheritdoc />
public override AnalyzerConfigOptions GlobalOptions { get; } public override AnalyzerConfigOptions GlobalOptions { get; }
/// <inheritdoc /> /// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException(); public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException();
/// <inheritdoc /> /// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException(); public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException();
} }
} }

View File

@ -1,202 +1,202 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using MapTo.Sources; using MapTo.Sources;
using MapTo.Tests.Extensions; using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure; using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Xunit; using Xunit;
using static MapTo.Tests.Common; using static MapTo.Tests.Common;
namespace MapTo.Tests namespace MapTo.Tests
{ {
public class MapPropertyTests public class MapPropertyTests
{ {
[Theory] [Theory]
[InlineData(NullableContextOptions.Disable)] [InlineData(NullableContextOptions.Disable)]
[InlineData(NullableContextOptions.Enable)] [InlineData(NullableContextOptions.Enable)]
public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions) public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions)
{ {
// Arrange // Arrange
const string source = ""; const string source = "";
var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty; var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty;
var languageVersion = nullableContextOptions == NullableContextOptions.Enable ? LanguageVersion.CSharp8 : LanguageVersion.CSharp7_3; var languageVersion = nullableContextOptions == NullableContextOptions.Enable ? LanguageVersion.CSharp8 : LanguageVersion.CSharp7_3;
var expectedInterface = $@" var expectedInterface = $@"
{Constants.GeneratedFilesHeader} {Constants.GeneratedFilesHeader}
{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)} {(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)}
using System; using System;
namespace MapTo namespace MapTo
{{ {{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = true)]
public sealed class MapPropertyAttribute : Attribute public sealed class MapPropertyAttribute : Attribute
{{ {{
public string{nullableSyntax} SourcePropertyName {{ get; set; }} public string{nullableSyntax} SourcePropertyName {{ get; set; }}
}} }}
}} }}
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions, languageVersion: languageVersion); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions, languageVersion: languageVersion);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface); compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface);
} }
[Fact] [Fact]
public void When_MapPropertyFound_Should_UseItToMapToSourceProperty() public void When_MapPropertyFound_Should_UseItToMapToSourceProperty()
{ {
// Arrange // Arrange
var source = GetSourceText(new SourceGeneratorOptions( var source = GetSourceText(new SourceGeneratorOptions(
true, true,
PropertyBuilder: builder => PropertyBuilder: builder =>
{ {
builder builder
.WriteLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]") .WriteLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]")
.WriteLine("public int Prop4 { get; set; }"); .WriteLine("public int Prop4 { get; set; }");
}, },
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @" var expectedResult = @"
partial class Foo partial class Foo
{ {
public Foo(Test.Models.Baz baz) public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { } : this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz) private protected Foo(MappingContext context, Test.Models.Baz baz)
{ {
if (context == null) throw new ArgumentNullException(nameof(context)); if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz)); if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this); context.Register(baz, this);
Prop1 = baz.Prop1; Prop1 = baz.Prop1;
Prop2 = baz.Prop2; Prop2 = baz.Prop2;
Prop3 = baz.Prop3; Prop3 = baz.Prop3;
Prop4 = baz.Prop3; Prop4 = baz.Prop3;
} }
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
} }
[Theory] [Theory]
[MemberData(nameof(MapPropertyWithImplicitConversionFoundData))] [MemberData(nameof(MapPropertyWithImplicitConversionFoundData))]
public void When_MapPropertyWithImplicitConversionFound_Should_UseItToMapToSourceProperty(string source, string expectedResult, LanguageVersion languageVersion) public void When_MapPropertyWithImplicitConversionFound_Should_UseItToMapToSourceProperty(string source, string expectedResult, LanguageVersion languageVersion)
{ {
// Arrange // Arrange
source = source.Trim(); source = source.Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
} }
public static IEnumerable<object[]> MapPropertyWithImplicitConversionFoundData => new List<object[]> public static IEnumerable<object[]> MapPropertyWithImplicitConversionFoundData => new List<object[]>
{ {
new object[] new object[]
{ {
@" @"
namespace Test namespace Test
{ {
using System.Collections.Generic; using System.Collections.Generic;
public class InnerClass { public int Prop1 { get; set; } } public class InnerClass { public int Prop1 { get; set; } }
public class OuterClass public class OuterClass
{ {
public int Id { get; set; } public int Id { get; set; }
public List<InnerClass> InnerProp { get; set; } public List<InnerClass> InnerProp { get; set; }
} }
} }
namespace Test.Models namespace Test.Models
{ {
using MapTo; using MapTo;
using System.Collections.Generic; using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))] [MapFrom(typeof(Test.InnerClass))]
public partial class InnerClass { public int Prop1 { get; set; } } public partial class InnerClass { public int Prop1 { get; set; } }
[MapFrom(typeof(Test.OuterClass))] [MapFrom(typeof(Test.OuterClass))]
public partial class OuterClass public partial class OuterClass
{ {
public int Id { get; set; } public int Id { get; set; }
public IReadOnlyList<InnerClass> InnerProp { get; set; } public IReadOnlyList<InnerClass> InnerProp { get; set; }
} }
} }
", ",
@" @"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass) private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
{ {
if (context == null) throw new ArgumentNullException(nameof(context)); if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass)); if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this); context.Register(outerClass, this);
Id = outerClass.Id; Id = outerClass.Id;
InnerProp = outerClass.InnerProp.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList(); InnerProp = outerClass.InnerProp.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList();
} }
", ",
LanguageVersion.CSharp7_3 LanguageVersion.CSharp7_3
}, },
new object[] new object[]
{ {
@" @"
namespace Test namespace Test
{ {
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
public class InnerClass public class InnerClass
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
} }
public class OuterClass public class OuterClass
{ {
public int Id { get; set; } public int Id { get; set; }
public List<InnerClass> InnerClasses { get; set; } public List<InnerClass> InnerClasses { get; set; }
public DateTime? SomeDate { get; set; } public DateTime? SomeDate { get; set; }
} }
} }
namespace Test.Models namespace Test.Models
{ {
using MapTo; using MapTo;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))] [MapFrom(typeof(Test.InnerClass))]
public partial record InnerClass(int Id, string Name); public partial record InnerClass(int Id, string Name);
[MapFrom(typeof(Test.OuterClass))] [MapFrom(typeof(Test.OuterClass))]
public partial record OuterClass(int Id, DateTime? SomeDate, IReadOnlyList<InnerClass> InnerClasses); public partial record OuterClass(int Id, DateTime? SomeDate, IReadOnlyList<InnerClass> InnerClasses);
} }
", ",
@" @"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass) private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
: this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList()) : this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList())
{ {
if (context == null) throw new ArgumentNullException(nameof(context)); if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass)); if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this); context.Register(outerClass, this);
} }
", ",
LanguageVersion.CSharp9 LanguageVersion.CSharp9
} }
}; };
} }
} }

View File

@ -1,42 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>MapTo.Tests</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Update="Nerdbank.GitVersioning">
<Version>3.5.109</Version>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Shouldly" Version="4.0.3" /> <PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2"> <PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.MapTo.csproj" /> <ProjectReference Include="..\..\src\MapTo\MapTo.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,283 +1,283 @@
using System.Linq; using System.Linq;
using MapTo.Sources; using MapTo.Sources;
using MapTo.Tests.Extensions; using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure; using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Xunit; using Xunit;
using static MapTo.Tests.Common; using static MapTo.Tests.Common;
namespace MapTo.Tests namespace MapTo.Tests
{ {
public class MapTypeConverterTests public class MapTypeConverterTests
{ {
[Fact] [Fact]
public void VerifyMapTypeConverterAttribute() public void VerifyMapTypeConverterAttribute()
{ {
// Arrange // Arrange
const string source = ""; const string source = "";
var expectedInterface = $@" var expectedInterface = $@"
{Constants.GeneratedFilesHeader} {Constants.GeneratedFilesHeader}
using System; using System;
namespace MapTo namespace MapTo
{{ {{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute public sealed class MapTypeConverterAttribute : Attribute
{{ {{
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null) public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
{{ {{
Converter = converter; Converter = converter;
ConverterParameters = converterParameters; ConverterParameters = converterParameters;
}} }}
public Type Converter {{ get; }} public Type Converter {{ get; }}
public object[] ConverterParameters {{ get; }} public object[] ConverterParameters {{ get; }}
}} }}
}} }}
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
} }
[Fact] [Fact]
public void VerifyMapTypeConverterAttributeWithNullableOptionOn() public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
{ {
// Arrange // Arrange
const string source = ""; const string source = "";
var expectedInterface = $@" var expectedInterface = $@"
{Constants.GeneratedFilesHeader} {Constants.GeneratedFilesHeader}
#nullable enable #nullable enable
using System; using System;
namespace MapTo namespace MapTo
{{ {{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute public sealed class MapTypeConverterAttribute : Attribute
{{ {{
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null) public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
{{ {{
Converter = converter; Converter = converter;
ConverterParameters = converterParameters; ConverterParameters = converterParameters;
}} }}
public Type Converter {{ get; }} public Type Converter {{ get; }}
public object[]? ConverterParameters {{ get; }} public object[]? ConverterParameters {{ get; }}
}} }}
}} }}
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface); compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
} }
[Fact] [Fact]
public void VerifyTypeConverterInterface() public void VerifyTypeConverterInterface()
{ {
// Arrange // Arrange
const string source = ""; const string source = "";
var expectedInterface = $@" var expectedInterface = $@"
{Constants.GeneratedFilesHeader} {Constants.GeneratedFilesHeader}
namespace MapTo namespace MapTo
{{ {{
public interface ITypeConverter<in TSource, out TDestination> public interface ITypeConverter<in TSource, out TDestination>
{{ {{
TDestination Convert(TSource source, object[] converterParameters); TDestination Convert(TSource source, object[] converterParameters);
}} }}
}} }}
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
} }
[Fact] [Fact]
public void VerifyTypeConverterInterfaceWithNullableOptionOn() public void VerifyTypeConverterInterfaceWithNullableOptionOn()
{ {
// Arrange // Arrange
const string source = ""; const string source = "";
var expectedInterface = $@" var expectedInterface = $@"
{Constants.GeneratedFilesHeader} {Constants.GeneratedFilesHeader}
#nullable enable #nullable enable
namespace MapTo namespace MapTo
{{ {{
public interface ITypeConverter<in TSource, out TDestination> public interface ITypeConverter<in TSource, out TDestination>
{{ {{
TDestination Convert(TSource source, object[]? converterParameters); TDestination Convert(TSource source, object[]? converterParameters);
}} }}
}} }}
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface); compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
} }
[Fact] [Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties() public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties()
{ {
// Arrange // Arrange
var source = GetSourceText(new SourceGeneratorOptions( var source = GetSourceText(new SourceGeneratorOptions(
true, true,
PropertyBuilder: builder => PropertyBuilder: builder =>
{ {
builder builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]") .WriteLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]")
.WriteLine("public string Prop4 { get; set; }"); .WriteLine("public string Prop4 { get; set; }");
}, },
SourcePropertyBuilder: builder => builder.WriteLine("public long Prop4 { get; set; }"))); SourcePropertyBuilder: builder => builder.WriteLine("public long Prop4 { get; set; }")));
source += @" source += @"
namespace Test namespace Test
{ {
using MapTo; using MapTo;
public class Prop4Converter: ITypeConverter<long, string> public class Prop4Converter: ITypeConverter<long, string>
{ {
public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string); 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 });"; const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });";
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
} }
[Fact] [Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties() public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties()
{ {
// Arrange // Arrange
var source = GetSourceText(new SourceGeneratorOptions( var source = GetSourceText(new SourceGeneratorOptions(
true, true,
PropertyBuilder: builder => PropertyBuilder: builder =>
{ {
builder builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]") .WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }"); .WriteLine("public long Prop4 { get; set; }");
}, },
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }"))); SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @" source += @"
namespace Test namespace Test
{ {
using MapTo; using MapTo;
public class Prop4Converter: ITypeConverter<string, long> public class Prop4Converter: ITypeConverter<string, long>
{ {
public long Convert(string source, object[] converterParameters) => long.Parse(source); public long Convert(string source, object[] converterParameters) => long.Parse(source);
} }
} }
"; ";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);"; const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);";
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax); compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
} }
[Fact] [Fact]
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty() public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
{ {
// Arrange // Arrange
var source = GetSourceText(new SourceGeneratorOptions( var source = GetSourceText(new SourceGeneratorOptions(
true, true,
PropertyBuilder: builder => { builder.WriteLine("public long Prop4 { get; set; }"); }, PropertyBuilder: builder => { builder.WriteLine("public long Prop4 { get; set; }"); },
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }"))); SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @" var expectedResult = @"
partial class Foo partial class Foo
{ {
public Foo(Test.Models.Baz baz) public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { } : this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz) private protected Foo(MappingContext context, Test.Models.Baz baz)
{ {
if (context == null) throw new ArgumentNullException(nameof(context)); if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz)); if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this); context.Register(baz, this);
Prop1 = baz.Prop1; Prop1 = baz.Prop1;
Prop2 = baz.Prop2; Prop2 = baz.Prop2;
Prop3 = baz.Prop3; Prop3 = baz.Prop3;
Prop4 = baz.Prop4; Prop4 = baz.Prop4;
} }
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult); compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
} }
[Fact] [Fact]
public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError() public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError()
{ {
// Arrange // Arrange
var source = GetSourceText(new SourceGeneratorOptions( var source = GetSourceText(new SourceGeneratorOptions(
true, true,
PropertyBuilder: builder => PropertyBuilder: builder =>
{ {
builder builder
.WriteLine("[IgnoreProperty]") .WriteLine("[IgnoreProperty]")
.WriteLine("public long IgnoreMe { get; set; }") .WriteLine("public long IgnoreMe { get; set; }")
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]") .WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }"); .WriteLine("public long Prop4 { get; set; }");
}, },
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }"))); SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @" source += @"
namespace Test namespace Test
{ {
using MapTo; using MapTo;
public class Prop4Converter: ITypeConverter<string, int> public class Prop4Converter: ITypeConverter<string, int>
{ {
public int Convert(string source, object[] converterParameters) => int.Parse(source); public int Convert(string source, object[] converterParameters) => int.Parse(source);
} }
} }
"; ";
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
var expectedError = DiagnosticsFactory.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz")); var expectedError = DiagnosticsFactory.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz"));
diagnostics.ShouldNotBeSuccessful(expectedError); diagnostics.ShouldNotBeSuccessful(expectedError);
} }
} }
} }

View File

@ -1,102 +1,102 @@
using MapTo.Sources; using MapTo.Sources;
using MapTo.Tests.Extensions; using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure; using MapTo.Tests.Infrastructure;
using Xunit; using Xunit;
using static MapTo.Tests.Common; using static MapTo.Tests.Common;
namespace MapTo.Tests namespace MapTo.Tests
{ {
public class MappingContextTests public class MappingContextTests
{ {
[Fact] [Fact]
public void VerifyMappingContextSource() public void VerifyMappingContextSource()
{ {
// Arrange // Arrange
const string source = ""; const string source = "";
var expected = @" var expected = @"
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
namespace MapTo namespace MapTo
{ {
internal sealed class MappingContext internal sealed class MappingContext
{ {
private readonly Dictionary<object, object> _cache; private readonly Dictionary<object, object> _cache;
internal MappingContext() internal MappingContext()
{ {
_cache = new Dictionary<object, object>(1); _cache = new Dictionary<object, object>(1);
} }
internal static TMapped Create<TOriginal, TMapped>(TOriginal original) internal static TMapped Create<TOriginal, TMapped>(TOriginal original)
{ {
if (original == null) throw new ArgumentNullException(nameof(original)); if (original == null) throw new ArgumentNullException(nameof(original));
var context = new MappingContext(); var context = new MappingContext();
var mapped = context.MapFromWithContext<TOriginal, TMapped>(original); var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);
if (mapped == null) if (mapped == null)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
return mapped; return mapped;
} }
internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original) internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)
{ {
if (original == null) if (original == null)
{ {
return default(TMapped); return default(TMapped);
} }
if (!TryGetValue<TOriginal, TMapped>(original, out var mapped)) if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))
{ {
var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null); var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);
if (instance != null) if (instance != null)
{ {
mapped = (TMapped)instance; mapped = (TMapped)instance;
} }
} }
return mapped; return mapped;
} }
internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped) internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)
{ {
if (original == null) throw new ArgumentNullException(nameof(original)); if (original == null) throw new ArgumentNullException(nameof(original));
if (mapped == null) throw new ArgumentNullException(nameof(mapped)); if (mapped == null) throw new ArgumentNullException(nameof(mapped));
if (!_cache.ContainsKey(original)) if (!_cache.ContainsKey(original))
{ {
_cache.Add(original, mapped); _cache.Add(original, mapped);
} }
} }
private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped) private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)
{ {
if (original != null && _cache.TryGetValue(original, out var value)) if (original != null && _cache.TryGetValue(original, out var value))
{ {
mapped = (TMapped)value; mapped = (TMapped)value;
return true; return true;
} }
mapped = default(TMapped); mapped = default(TMapped);
return false; return false;
} }
} }
} }
".Trim(); ".Trim();
// Act // Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions); var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert // Assert
diagnostics.ShouldBeSuccessful(); diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MappingContextSource.ClassName, expected); compilation.SyntaxTrees.ShouldContainSource(MappingContextSource.ClassName, expected);
} }
} }
} }

View File

@ -1,59 +0,0 @@
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;
}
}
}

View File

@ -1,16 +0,0 @@
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;
}
}

View File

@ -1,22 +0,0 @@
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;
}
}

View File

@ -1,27 +0,0 @@
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;
}
}
}

View File

@ -1,21 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace TestConsoleApp.Data.Models namespace TestConsoleApp.Data.Models
{ {
public class Employee
public class Employee {
{ public int Id { get; set; }
public int Id { get; }
public string EmployeeCode { get; set; }
public string EmployeeCode { get; }
public Manager Manager { get; set; }
public Employee(int id, string employeeCode) }
{ }
Id = id;
EmployeeCode = employeeCode;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace TestConsoleApp.Data.Models
{
public class Manager: Employee
{
public int Level { get; set; }
public IEnumerable<Employee> Employees { get; set; } = Array.Empty<Employee>();
}
}

View File

@ -1,26 +0,0 @@
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;
}
}
}

View File

@ -0,0 +1,11 @@
namespace TestConsoleApp.Data.Models
{
public class Profile
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace TestConsoleApp.Data.Models
{
public class User
{
public int Id { get; set; }
public DateTimeOffset RegisteredAt { get; set; }
public Profile Profile { get; set; }
}
}

View File

@ -1,44 +0,0 @@

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<FinanceTransaction> FinanceTransactions;
public User(int id, string name, string address, string btcAddress, string ltcAddress, double btcAmount, double ltcAmount, List<FinanceTransaction> 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);
}
}
}

View File

@ -1,16 +0,0 @@
using System.Collections.Generic;
namespace BlueWest.Data
{
public class UserList
{
public List<User> Users;
public UserList(List<User> users)
{
Users = users;
}
public int Length => Users.Count;
}
}

View File

@ -1,19 +0,0 @@
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;
}
}

View File

@ -1,32 +1,92 @@
using System; using System;
using MapTo; using MapTo;
using TestConsoleApp.Data.Models; using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels; using TestConsoleApp.ViewModels;
using TestConsoleApp.ViewModels2;
namespace TestConsoleApp
{ namespace TestConsoleApp
internal class Program {
{ internal class Program
private static void Main(string[] args) {
{ private static void Main(string[] args)
//UserTest(); {
//UserTest();
// EmployeeManagerTest(); CyclicReferenceTest();
Console.WriteLine("done");
} // EmployeeManagerTest();
Console.WriteLine("done");
private static void EmployeeManagerTest() }
{
private static void EmployeeManagerTest()
{
var employee = new Employee(1, "hello"); var manager1 = new Manager
{
Id = 1,
EmployeeCode = "M001",
Level = 100
} };
var manager2 = new Manager
{
} Id = 2,
EmployeeCode = "M002",
Level = 100,
Manager = manager1
};
var employee1 = new Employee
{
Id = 101,
EmployeeCode = "E101",
Manager = manager1
};
var employee2 = new Employee
{
Id = 102,
EmployeeCode = "E102",
Manager = manager2
};
manager1.Employees = new[] { employee1, manager2 };
manager2.Employees = new[] { employee2 };
manager1.ToManagerViewModel();
employee1.ToEmployeeViewModel();
}
private static ManagerViewModel CyclicReferenceTest()
{
var manager1 = new Manager
{
Id = 1,
EmployeeCode = "M001",
Level = 100
};
manager1.Manager = manager1;
return manager1.ToManagerViewModel();
}
private static void UserTest()
{
var user = new User
{
Id = 1234,
RegisteredAt = DateTimeOffset.Now,
Profile = new Profile
{
FirstName = "John",
LastName = "Doe"
}
};
var vm = user.ToUserViewModel();
Console.WriteLine("Key: {0}", vm.Key);
Console.WriteLine("RegisteredAt: {0}", vm.RegisteredAt);
Console.WriteLine("FirstName: {0}", vm.Profile.FirstName);
Console.WriteLine("LastName: {0}", vm.Profile.LastName);
}
}
} }

View File

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net471</TargetFramework> <TargetFramework>net471</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> <ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\src\BlueWest.MapTo\MapTo.props" /> <Import Project="..\..\src\MapTo\MapTo.props" />
<PropertyGroup> <PropertyGroup>
<MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier> <MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -1,23 +0,0 @@
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;
}
}
}

View File

@ -1,13 +1,16 @@
using MapTo; using MapTo;
using TestConsoleApp.Data.Models; using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels2;
namespace TestConsoleApp.ViewModels
{ namespace TestConsoleApp.ViewModels
[MapFrom(typeof(Employee))] {
public partial class EmployeeViewModel [MapFrom(typeof(Employee))]
{ public partial class EmployeeViewModel
public int Id { get; } {
public int Id { get; set; }
} public string EmployeeCode { get; set; }
}
public ManagerViewModel Manager { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using MapTo;
using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels;
namespace TestConsoleApp.ViewModels2
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}

View File

@ -1,22 +0,0 @@
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;
}
}
}

View File

@ -0,0 +1,13 @@
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Profile))]
public partial class ProfileViewModel
{
public string FirstName { get; }
public string LastName { get; }
}
}

View File

@ -0,0 +1,24 @@
using System;
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.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<int, string>
{
public string Convert(int source, object[]? converterParameters) => $"{source:X}";
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"version": "0.9", "version": "0.8",
"semVer1NumericIdentifierPadding": 1, "semVer1NumericIdentifierPadding": 1,
"publicReleaseRefSpec": [ "publicReleaseRefSpec": [
"^refs/heads/master$", "^refs/heads/master$",