diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..060ac76 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,329 @@ +root = true +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file + +# Global settings +[*] +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_style = space +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_style = space +indent_size = 2 + +[*.{md,json}] +indent_style = space +indent_size = 4 + +[*.cs] +indent_style = space +indent_size = 4 +max_line_length = 130 + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# only use var when it's obvious what the variable type is +csharp_style_var_for_built_in_types = false:none +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion + +# use language keywords instead of BCL types +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:warning +dotnet_sort_system_directives_first = true +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:error + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:error + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_attribute_sections = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +resharper_blank_lines_after_multiline_statements = 1 + +# Purpose: Don't put elements of an array on new lines, unless the length exceeds the maximum line length. +resharper_wrap_array_initializer_style = chop_always + +# Purpose: An item within a C# enumeration is missing an Xml documentation header +# Reason: Some enums are self-explanatory +dotnet_diagnostic.ca1062.severity = suggestion + +# Purpose: Specify StringComparison +# Reason: Ignored since we force invariant culture on the csproj level +dotnet_diagnostic.ca1307.severity = none + +# Purpose: Specify StringComparison +# Reason: Specify StringComparison for methods that don't use Ordinal by default +dotnet_diagnostic.ca1310.severity = error + +# Purpose: 'X' is an internal class that is apparently never instantiated. If so, remove the code from the assembly +# Reason: The class is used, but not directly through the constructor, which is fine. +dotnet_diagnostic.ca1812.severity = none + +# Purpose: Properties should not return arrays +# Reason: Although it's good advice, we should decide this case-by-case +dotnet_diagnostic.ca1819.severity = suggestion + +# Purpose: For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' +# Reason: For now, we prefer to use explicit checks using IsEnabled +dotnet_diagnostic.ca1848.severity = none + +# Purpose: Use Task.ConfigureAwait(false) if the current SynchronizationContext is not needed +# Reason: Not relevant for .NET Core web applications. +dotnet_diagnostic.ca2007.severity = none + +# Purpose: # SA0001: XmlCommentAnalysisDisabled +# Reason: We don't want to force this +dotnet_diagnostic.sa0001.severity = none + +# Purpose: Use string.Empty for empty string +# Reason: We don't want to force this +dotnet_diagnostic.sa1122.severity = none + +# Purpose: Prefix local calls with this +# Reason: We don't want to force this +dotnet_diagnostic.sa1101.severity = none + +# Purpose: A enum should not follow a class +# Reason: We don't want to force this for a content-only package +dotnet_diagnostic.sa1201.severity = none + +# Purpose: public' members should come before 'private' members +# Reason: We keep members in order of execution +dotnet_diagnostic.SA1202.severity = none + +# Purpose: Static members should appear before non-static members +# Reason: We keep members in order of execution +dotnet_diagnostic.SA1204.severity = none + +# Purpose: Code should not contain multiple blank lines in a row +# Reason: No need to fail a build on that +dotnet_diagnostic.sa1507.severity = none + +# Purpose: A single-line comment within C# code is not preceded by a blank line. +# Reason: Unnecessarily strict +dotnet_diagnostic.sa1515.severity = none + +# Purpose: File is required to end with a single newline character +# Reason: We don't case +dotnet_diagnostic.sa1518.severity = none + +# Purpose: Element parameters should be documented +# Reason: We don't want to force this +dotnet_diagnostic.sa1611.severity = none + +# Purpose: Element return value should be documented +# Reason: We don't want to force this +dotnet_diagnostic.sa1615.severity = none + +# Purpose: Generic type parameters should be documented +# Reason: We don't want to force this +dotnet_diagnostic.sa1618.severity = none + +# Purpose: Element return value should be documented +# Reason: We don't want to force this +dotnet_diagnostic.sa1622.severity = none + +# Purpose: The property's documentation summary text should begin with: 'Gets or sets' +# Reason: We don't want to force this +dotnet_diagnostic.sa1623.severity = none + +# Purpose: Documentation text should end with a period +# Reason: We're not building a library +dotnet_diagnostic.sa1629.severity = none + +# Purpose: The file header is missing or not located at the top of the file +# Reason: We don't want to force this +dotnet_diagnostic.sa1633.severity = none + +# Purpose: File name should match first type name +# Reason: To keep the content-only package simple, we keep everything together +dotnet_diagnostic.sa1649.severity = none + +# Purpose: Elements should be documented +# Reason: We don't want to force documentation for all elements +dotnet_diagnostic.sa1600.severity = suggestion + +# Purpose: Partial elements should be documented +# Reason: We don't want to force this +dotnet_diagnostic.sa1601.severity = none + +# Purpose: Enumeration items should be documented +# Reason: We don't want to force this +dotnet_diagnostic.sa1602.severity = suggestion + +# Purpose: Closing parenthesis should be on line of last parameter +# Reason: We don't want to force this +dotnet_diagnostic.sa1111.severity = none + +# Purpose: The parameters should begin on the line after the declaration, whenever the parameter span across multiple lines +# Reason: We don't want to force this +dotnet_diagnostic.sa1116.severity = none + +# Purpose: Use an overload of 'Contains' that has a StringComparison parameter +# Reason: Ignored since we force invariant culture on the csproj level +dotnet_diagnostic.ma0001.severity = none + +# Purpose: MA0002 : Use an overload that has a IEqualityComparer or IComparer parameter +# Reason: Ignored since we force invariant culture on the csproj level +dotnet_diagnostic.ma0002.severity = none + +# Purpose: Use Task.ConfigureAwait(false) if the current SynchronizationContext is not needed +# Reason: Not relevant for .NET Core web applications. +dotnet_diagnostic.ma0004.severity = none + +# Purpose: Use string.Equals instead of NotEquals operator +# Reason: Ignored since we force invariant culture on the csproj level +dotnet_diagnostic.ma0006.severity = none + +# Purpose: MA0011 : Use an overload of 'ToString' that has a 'System.IFormatProvider' parameter +# Reason: Ignored since we force invariant culture on the csproj level +dotnet_diagnostic.ma0011.severity = none + +# Purpose: Fix TODO comment +# Reason: We don't want to force this +dotnet_diagnostic.ma0026.severity = suggestion + +# Purpose: File name must match type name. +# Reason: Too many purposeful violations. +dotnet_diagnostic.MA0048.severity = none + +# Purpose: Closing parenthesis should not be preceded by a space +# Reason: We don't want to force this +dotnet_diagnostic.sa1009.severity = none + +# Purpose: Code should not contain trailing whitespace +# Reason: We don't want to force this +dotnet_diagnostic.sa1028.severity = none + +# Purpose: File may only contain a single type +# Reason: We keep everything together in this project. +dotnet_diagnostic.SA1402.severity = none + +# Purpose: Use trailing comma in multi-line initialize +# Reason: We don't want to force this +dotnet_diagnostic.sa1413.severity = none + +# Purpose: Method contains 8 statements, which exceeds the maximum of 7 statements +# Reason: We don't want to force this +dotnet_diagnostic.av1500.severity = none + +# Purpose: Loop statement contains nested loop +# Reason: We don't want to completely prevent this. +dotnet_diagnostic.av1532.severity = suggestion + +# Purpose: AV1555 : Parameter in the call is invoked with a named argument +# Reason: False positives. See https://github.com/bkoelman/CSharpGuidelinesAnalyzer/issues/145 +dotnet_diagnostic.av1555.severity = none + +# Purpose: Argument for parameter 'param' in method calls nested method +# Reason: Not a real issue in Rider anymore +dotnet_diagnostic.av1580.severity = none + +# Purpose: AV1706 : Parameter 'p' should have a more descriptive name +# Reason: Also complains about lambda parameters. See https://github.com/bkoelman/CSharpGuidelinesAnalyzer/issues/147 +dotnet_diagnostic.av1706.severity = none + +# Purpose: Property 'Username' contains the name of its containing type 'User'. We should make that decision case-by-case +# Reason: We don't want to judge this case-by-case +dotnet_diagnostic.av1710.severity = none + +# Purpose: Error AV1755 : Name of async method should end with Async or TaskAsync +# Reason: We don't want to force this +dotnet_diagnostic.av1755.severity = none + +# Purpose: Missing XML comment for internally visible type or member +# Reason: We don't want to force this +dotnet_diagnostic.av2305.severity = suggestion + +# Purpose: Pass -warnaserror to the compiler or add True to your project +# Reason: It is already enabled, but during a Sonar scan, the scanner switches back causing unnecessary warnings +dotnet_diagnostic.av2210.severity = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7c59b39 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,65 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +*.sh text eol=lf + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..99db83d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: dennisdoomen +ko_fi: dennisdoomen diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d054656 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "16:00" + timezone: "Europe/Copenhagen" + + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "16:00" + timezone: "Europe/Copenhagen" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..859710d --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,29 @@ +changelog: + exclude: + labels: + - changelog-ignore + authors: [ ] + categories: + - title: Breaking Changes + labels: + - "breaking change" + - title: New features + labels: + - "feature" + - title: Improvements + labels: + - "enhancement" + - "performance" + - title: Fixes + labels: + - "bug" + - "regression" + - title: Fixes (extensibility) + labels: + - "extensibility" + - title: Documentation + labels: + - "documentation" + - title: Others + labels: + - "*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bdf015a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,71 @@ +name: Build + +on: [push, pull_request] + +jobs: + build: + name: "Build, Test, Analyze and Publish" + runs-on: windows-latest + + env: + DOTNET_NOLOGO: false + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 3.0.x + 6.0.x + + - name: Run NUKE + run: ./build.ps1 + env: + NuGetApiKey: ${{ secrets.NUGETAPIKEY }} + + - name: Check for 'lcov.info' existence + id: check_files + uses: andstor/file-existence-action@v3 + with: + files: "TestResults/reports/lcov.info" + + - name: coveralls + uses: coverallsapp/github-action@v2 + if: steps.check_files.outputs.files_exists == 'true' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: TestResults/reports/lcov.info + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: windows-artifacts + path: | + ./Artifacts/* + ./TestResults/*.trx + + publish-test-results: + name: "Publish Tests Results" + needs: [ build ] + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + if: always() + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + comment_mode: always + files: "artifacts/**/**/*.trx" diff --git a/.gitignore b/.gitignore index 8a30d25..ee534ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,84 +1,41 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files -*.rsuser *.suo *.user -*.userosscache *.sln.docstates +*.sln.ide +.vs/ +.vscode/ -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* +project.lock.json +*.nuget.props +*.nuget.targets # Build results +Artifacts/ [Dd]ebug/ -[Dd]ebugPublic/ [Rr]elease/ -[Rr]eleases/ x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ [Bb]in/ [Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ -# Visual Studio 2017 auto generated files -Generated\ Files/ +# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets +!packages/*/build/ +packages/** # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio *_i.c *_p.c -*_h.h *.ilk *.meta *.obj -*.iobj *.pch *.pdb -*.ipdb *.pgc *.pgd *.rsp @@ -88,41 +45,27 @@ StyleCopReport.xml *.tlh *.tmp *.tmp_proj -*_wpftmp.csproj *.log -*.tlog +*.binlog *.vspscc *.vssscc .builds *.pidb -*.svclog +*.log *.scc -# Chutzpah Test files -_Chutzpah* - # Visual C++ cache files ipch/ *.aps *.ncb -*.opendb *.opensdf *.sdf *.cachefile -*.VC.db -*.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ # Guidance Automation Toolkit *.gpState @@ -130,7 +73,6 @@ $tf/ # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper -*.DotSettings.user # TeamCity is a build add-in _TeamCity* @@ -138,30 +80,9 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch -_NCrunch_* .*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ +_NCrunch_FluentAssertions/* # Installshield output folder [Ee]xpress/ @@ -180,219 +101,94 @@ DocProject/Help/html publish/ # Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets +*.Publish.xml -# Microsoft Azure Build Output -csx/ -*.build.csdef +# NuGet Packages Directory +packages/ -# Microsoft Azure Emulator -ecf/ -rcf/ +# Windows Azure Build Output +csx +*.build.csdef -# Windows Store app package directories and files +# Windows Store app package directory AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ # Others +sql/ +*.Cache ClientBin/ +[Ss]tyle[Cc]op.* ~$* *~ *.dbmdl -*.dbproj.schemaview -*.jfm +*.[Pp]ublish.xml *.pfx *.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ # RIA/Silverlight projects Generated_Code/ -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak # SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ +App_Data/*.mdf +App_Data/*.ldf -# CodeRush personal settings -.cr/personal -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc +#LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# ========================= +# Windows detritus +# ========================= -# Tabs Studio -*.tss +# Windows image file caches +Thumbs.db +ehthumbs.db -# Telerik's JustMock configuration file -*.jmconfig +# Folder config file +Desktop.ini -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# Recycle Bin used on file shares +$RECYCLE.BIN/ -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml +# Mac desktop service store files +.DS_Store +Package/Lib +*.nupkg +*.zip + +.idea* + +# Jekyll +*.gem +*.sublime-project +*.sublime-workspace +docs/.bundle +docs/.DS_Store +docs/.jekyll-metadata +docs/.sass-cache +docs/_asset_bundler_cache +docs/_site +docs/codekit-config.json +docs/Gemfile.lock +docs/node_modules + +# Approval Tests +*.received.txt + +# Documentation file needed for StyleCop.Analyzers +Tests/FluentAssertions.Specs/FluentAssertions.Specs.xml + +# BenchmarkDotNet +Tests/Benchmarks/BenchmarkDotNet.Artifacts/ + +# Documentation spell check +node_modules/ \ No newline at end of file diff --git a/.idea/.idea.reflectorator/.idea/.gitignore b/.idea/.idea.reflectorator/.idea/.gitignore new file mode 100644 index 0000000..e363d7b --- /dev/null +++ b/.idea/.idea.reflectorator/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/.idea.reflectorator.iml +/modules.xml +/contentModel.xml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.reflectorator/.idea/indexLayout.xml b/.idea/.idea.reflectorator/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.reflectorator/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.reflectorator/.idea/vcs.xml b/.idea/.idea.reflectorator/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.reflectorator/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..06d267a --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "NuGetApiKey": { + "type": "string", + "description": "The key to push to Nuget", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + } + }, + "definitions": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "CalculateNugetVersion", + "CodeCoverage", + "Compile", + "Default", + "Pack", + "Push", + "Restore", + "RunTests" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "$ref": "#/definitions/NukeBuild" +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..e444003 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "build.schema.json", + "Solution": "Reflectify.sln" +} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..ee77e52 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,44 @@ + + + 11.0 + false + $(WarningsNotAsErrors);NU1902;NU1903 + 1591;1573 + true + + + + + false + false + false + + + true + 7.0 + All + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..0440058 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,19 @@ +workflow: GitHubFlow/v1 +branches: + develop: + regex: ^dev(elop)?(ment)?$ + label: alpha + increment: Minor + release: + regex: releases?[/-] + label: rc + increment: Patch + pull-request: + mode: ContinuousDelivery + regex: ((pull|pull\-requests|pr)[/-]|[/-](merge)) + label: pr + label-number-pattern: '[/-]?(?\d+)' + prevent-increment: + of-merged-branch: false +ignore: + sha: [] diff --git a/README.md b/README.md new file mode 100644 index 0000000..bce8dc0 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Reflectify - Reflection extensions without causing dependency pains + +[![](https://img.shields.io/github/actions/workflow/status/dennisdoomen/reflectify/build.yml?branch=main)](https://github.com/dennisdoomen/reflectify/actions?query=branch%3amain) +[![Coveralls branch](https://img.shields.io/coverallsCoverage/github/dennisdoomen/reflectify?branch=main)](https://coveralls.io/github/dennisdoomen/reflectify?branch=main) +[![](https://img.shields.io/github/release/DennisDoomen/Reflectify.svg?label=latest%20release&color=007edf)](https://github.com/dennisdoomen/reflectify/releases/latest) +[![](https://img.shields.io/nuget/dt/Reflectify.svg?label=downloads&color=007edf&logo=nuget)](https://www.nuget.org/packages/Reflectify) +[![](https://img.shields.io/librariesio/dependents/nuget/Reflectify.svg?label=dependent%20libraries)](https://libraries.io/nuget/Reflectify) +[![GitHub Repo stars](https://img.shields.io/github/stars/dennisdoomen/reflectify)](https://github.com/dennisdoomen/reflectify/stargazers) +[![GitHub contributors](https://img.shields.io/github/contributors/dennisdoomen/reflectify)](https://github.com/dennisdoomen/reflectify/graphs/contributors) +[![GitHub last commit](https://img.shields.io/github/last-commit/dennisdoomen/reflectify)](https://github.com/dennisdoomen/reflectify) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/dennisdoomen/reflectify)](https://github.com/dennisdoomen/reflectify/graphs/commit-activity) +[![open issues](https://img.shields.io/github/issues/dennisdoomen/reflectify)](https://github.com/dennisdoomen/reflectify/issues) +![](https://img.shields.io/badge/release%20strategy-githubflow-orange.svg) + +> [!TIP] +> If you like this package, consider sponsoring me through [Github Sponsors](https://github.com/sponsors/dennisdoomen) + +## What's this about? + +Reflectify offers a bunch of extension methods to provide information such as the properties or fields a type exposes and metadata about those members. It supports all major .NET versions and even understands explicltly implemented properties or properties coming from default interface implementations, a C# 8 feature. + +## What's so special about that? + +Nothing really, but it offers that functionality through a content-only NuGet package. In other words, you can use this package in your own packages, without the need to tie yourself to the Reflectify package. diff --git a/Reflectify.sln b/Reflectify.sln new file mode 100644 index 0000000..3edf8bc --- /dev/null +++ b/Reflectify.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reflectify", "src\Reflectify\Reflectify.csproj", "{C40328B9-91F9-491A-8831-AAE00D62980F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reflectify.Specs", "tests\Reflectify.Specs\Reflectify.Specs.csproj", "{07AE886E-509D-49C3-BB61-EC62903FDD90}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{506022A4-AD68-43A1-A670-F2798A4091FB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {506022A4-AD68-43A1-A670-F2798A4091FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {506022A4-AD68-43A1-A670-F2798A4091FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C40328B9-91F9-491A-8831-AAE00D62980F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C40328B9-91F9-491A-8831-AAE00D62980F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C40328B9-91F9-491A-8831-AAE00D62980F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C40328B9-91F9-491A-8831-AAE00D62980F}.Release|Any CPU.Build.0 = Release|Any CPU + {07AE886E-509D-49C3-BB61-EC62903FDD90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07AE886E-509D-49C3-BB61-EC62903FDD90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07AE886E-509D-49C3-BB61-EC62903FDD90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07AE886E-509D-49C3-BB61-EC62903FDD90}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Reflectify.sln.DotSettings b/Reflectify.sln.DotSettings new file mode 100644 index 0000000..ab64a3d --- /dev/null +++ b/Reflectify.sln.DotSettings @@ -0,0 +1,193 @@ + + True + True + False + False + True + HINT + DO_NOT_SHOW + <?xml version="1.0" encoding="utf-16"?><Profile name="Safe Cleanup"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" RemoveRedundantParentheses="True" /><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + &lt;option name="myName" value="Safe Cleanup" /&gt; + &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSPrimitiveTypeWrapperUsage" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSRemoveUnnecessaryParentheses" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="JSUnnecessarySemicolon" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="TypeScriptExplicitMemberType" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryContinueJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelOnBreakStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryLabelOnContinueStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="UnnecessaryReturnJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; + &lt;inspection_tool class="WrongPropertyKeyValueDelimiter" enabled="false" level="WEAK WARNING" enabled_by_default="false" /&gt; +&lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id="CSS"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="HTTP Request"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Handlebars"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Ini"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Jade"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JavaScript"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="Markdown"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="PowerShell"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Properties"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="SQL"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="VueExpr"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;Rearrange&gt;false&lt;/Rearrange&gt; + &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; + &lt;/Language&gt; + &lt;Language id="yaml"&gt; + &lt;Reformat&gt;false&lt;/Reformat&gt; + &lt;/Language&gt; +&lt;/profile&gt;</RIDER_SETTINGS><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags></Profile> + Safe Cleanup + Required + Required + Required + Required + False + 1 + 1 + True + NEVER + False + NEVER + True + False + 130 + False + 1 + 130 + False + UseExplicitType + UseVarWhenEvident + UseExplicitType + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + + OUTLINE + SOLUTION_FOLDER + True + True + True + True + True + True + True + True + True + True + True + D:\Workspaces\FluentAssertions\Default.testsettings + 4 + False + True + False + True + 0 + + False + + aaa + Arrange-Act-Assert + [Fact] +public void $Fact$() +{ + // Arrange + $END$ + + // Act + + + // Assert +} + True + True + True + True + InCSharpFile + 2.0 + True + True + True + True + True diff --git a/build.cmd b/build.cmd new file mode 100755 index 0000000..b08cc59 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..4634dc0 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" + +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_NOLOGO = 1 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" +} + +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..fdff0c6 --- /dev/null +++ b/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 0000000..31e43dc --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 0000000..e107b04 --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,181 @@ +using System; +using System.Linq; +using Nuke.Common; +using Nuke.Common.CI; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.Execution; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitVersion; +using Nuke.Common.Tools.ReportGenerator; +using Nuke.Common.Utilities.Collections; +using Nuke.Components; +using YamlDotNet.Serialization; +using static Nuke.Common.EnvironmentInfo; +using static Nuke.Common.IO.FileSystemTasks; +using static Nuke.Common.IO.PathConstruction; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using static Nuke.Common.Tools.ReportGenerator.ReportGeneratorTasks; +using static Serilog.Log; + +class Build : NukeBuild +{ + /// Support plugins are available for: + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - Microsoft VSCode https://nuke.build/vscode + + public static int Main () => Execute(x => x.Default); + + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + GitHubActions GitHubActions => GitHubActions.Instance; + + string BranchSpec => GitHubActions?.Ref; + + string BuildNumber => GitHubActions?.RunNumber.ToString(); + + string PullRequestBase => GitHubActions?.BaseRef; + + [Parameter("The key to push to Nuget")] + [Secret] + readonly string NuGetApiKey; + + [Solution(GenerateProjects = true)] + readonly Solution Solution; + + [GitVersion(Framework = "net6.0", NoFetch = true)] + readonly GitVersion GitVersion; + + AbsolutePath ArtifactsDirectory => RootDirectory / "Artifacts"; + + AbsolutePath TestResultsDirectory => RootDirectory / "TestResults"; + + string SemVer; + + Target CalculateNugetVersion => _ => _ + .Executes(() => + { + SemVer = GitVersion.SemVer; + if (IsPullRequest) + { + Information( + "Branch spec {branchspec} is a pull request. Adding build number {buildnumber}", + BranchSpec, BuildNumber); + + SemVer = string.Join('.', GitVersion.SemVer.Split('.').Take(3).Union(new[] { BuildNumber })); + } + + Information("SemVer = {semver}", SemVer); + }); + + Target Restore => _ => _ + .Executes(() => + { + DotNetRestore(s => s + .SetProjectFile(Solution) + .EnableNoCache()); + }); + + Target Compile => _ => _ + .DependsOn(CalculateNugetVersion) + .DependsOn(Restore) + .Executes(() => + { + ReportSummary(s => s + .WhenNotNull(SemVer, (summary, semVer) => summary + .AddPair("Version", semVer))); + + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoLogo() + .EnableNoRestore() + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemFileVer) + .SetInformationalVersion(GitVersion.InformationalVersion)); + }); + + Target RunTests => _ => _ + .DependsOn(Compile) + .Executes(() => + { + TestResultsDirectory.CreateOrCleanDirectory(); + + DotNetTest(s => s + .SetConfiguration(Configuration.Debug) + .SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .SetDataCollector("XPlat Code Coverage") + .SetResultsDirectory(TestResultsDirectory) + .SetProjectFile(Solution.Reflectify_Specs) + .CombineWith(Solution.Reflectify_Specs.GetTargetFrameworks(), + (ss, framework) => ss + .SetFramework(framework) + .AddLoggers($"trx;LogFileName={framework}.trx") + )); + }); + + Target CodeCoverage => _ => _ + .DependsOn(RunTests) + .Executes(() => + { + ReportGenerator(s => s + .SetTargetDirectory(TestResultsDirectory / "reports") + .AddReports(TestResultsDirectory / "**/coverage.cobertura.xml") + .AddReportTypes(ReportTypes.lcov, ReportTypes.Html) + .AddFileFilters("-*.g.cs")); + + string link = TestResultsDirectory / "reports" / "index.html"; + Information($"Code coverage report: \x1b]8;;file://{link.Replace('\\', '/')}\x1b\\{link}\x1b]8;;\x1b\\"); + }); + + Target Pack => _ => _ + .DependsOn(CalculateNugetVersion) + .DependsOn(CodeCoverage) + .Executes(() => + { + ReportSummary(s => s + .WhenNotNull(SemVer, (c, semVer) => c + .AddPair("Packed version", semVer))); + + DotNetPack(s => s + .SetProject(Solution.Reflectify) + .SetOutputDirectory(ArtifactsDirectory) + .SetConfiguration(Configuration == Configuration.Debug ? "Debug" : "Release") + .EnableNoBuild() + .EnableNoLogo() + .EnableNoRestore() + .EnableContinuousIntegrationBuild() // Necessary for deterministic builds + .SetVersion(SemVer)); + }); + + Target Push => _ => _ + .DependsOn(Pack) + .OnlyWhenDynamic(() => IsTag) + .ProceedAfterFailure() + .Executes(() => + { + var packages = ArtifactsDirectory.GlobFiles("*.nupkg"); + + Assert.NotEmpty(packages); + + DotNetNuGetPush(s => s + .SetApiKey(NuGetApiKey) + .EnableSkipDuplicate() + .SetSource("https://api.nuget.org/v3/index.json") + .EnableNoSymbols() + .CombineWith(packages, + (v, path) => v.SetTargetPath(path))); + }); + + Target Default => _ => _ + .DependsOn(Push); + + bool IsPullRequest => GitHubActions?.IsPullRequest ?? false; + + bool IsTag => BranchSpec != null && BranchSpec.Contains("refs/tags", StringComparison.OrdinalIgnoreCase); +} diff --git a/build/Configuration.cs b/build/Configuration.cs new file mode 100644 index 0000000..9c08b1a --- /dev/null +++ b/build/Configuration.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Nuke.Common.Tooling; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} diff --git a/build/Directory.Build.props b/build/Directory.Build.props new file mode 100644 index 0000000..e147d63 --- /dev/null +++ b/build/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets new file mode 100644 index 0000000..2532609 --- /dev/null +++ b/build/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/build/_build.csproj b/build/_build.csproj new file mode 100644 index 0000000..a1833b6 --- /dev/null +++ b/build/_build.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + + CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006 + .. + .. + 1 + false + + + + + + + + + + diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings new file mode 100644 index 0000000..c815d36 --- /dev/null +++ b/build/_build.csproj.DotSettings @@ -0,0 +1,31 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + True + True + True + True + True + True + True + True + True + True diff --git a/global.json b/global.json new file mode 100644 index 0000000..368a14e --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.100", + "rollForward": "latestMajor" + } +} diff --git a/src/Reflectify/.nuspec b/src/Reflectify/.nuspec new file mode 100644 index 0000000..9058ddf --- /dev/null +++ b/src/Reflectify/.nuspec @@ -0,0 +1,24 @@ + + + + + Reflectify + $version$ + + true + Dennis Doomen + MIT + false + https://github.com/dennisdoomen/Reflectify + A source-only package to help get metadata from .NET types + source reflection + + + + + + + + + + diff --git a/src/Reflectify/Reflectify.cs b/src/Reflectify/Reflectify.cs new file mode 100644 index 0000000..9c9f611 --- /dev/null +++ b/src/Reflectify/Reflectify.cs @@ -0,0 +1,230 @@ +#if !REFLECTIFY_COMPILE +// +#pragma warning disable +#endif + +#nullable disable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Reflectify; + +public static class TypeExtensions +{ + private static readonly ConcurrentDictionary<(Type Type, MemberKind Kind), Reflector> ReflectorCache = new(); + + /// + /// Gets the public, internal, explicitly implemented and/or default properties of a type hierarchy. + /// + /// The type to reflect. + /// The kind of properties to include in the response. + public static IEnumerable GetProperties(this Type type, MemberKind kind) + { + return GetFor(type, kind).Properties; + } + + /// + /// Gets the public and/or internal fieldss of a type hierarchy. + /// + /// The type to reflect. + /// The kind of fields to include in the response. + public static IEnumerable GetFields(this Type type, MemberKind kind) + { + return GetFor(type, kind).Fields; + } + + /// + /// Gets a combination of and . + /// + /// The type to reflect. + /// The kind of fields and properties to include in the response. + public static IEnumerable GetMembers(this Type type, MemberKind kind) + { + return GetFor(type, kind).Members; + } + + + private static Reflector GetFor(Type typeToReflect, MemberKind kind) + { + return ReflectorCache.GetOrAdd((typeToReflect, kind), + static key => new Reflector(key.Type, key.Kind)); + } +} + +public static class PropertyInfoExtensions +{ + /// + /// Returns true if the property is an indexer, or false otherwise. + /// + public static bool IsIndexer(this PropertyInfo member) + { + return member.GetIndexParameters().Length != 0; + } + + /// + /// Returns true if the property is explicitly implemented on the + /// , or false otherwise. + /// + public static bool IsExplicitlyImplemented(this PropertyInfo prop) + { +#if NETCOREAPP3_0_OR_GREATER + return prop.Name.Contains('.', StringComparison.OrdinalIgnoreCase); +#else + return prop.Name.Contains('.'); +#endif + } +} + +/// +/// Defines the kinds of members you want to get when querying for the fields and properties of a type. +/// +[Flags] +public enum MemberKind +{ + Public = 1, + Internal = 2, + ExplicitlyImplemented = 4, + DefaultInterfaceProperties = 8 +} + +/// +/// Helper class to get all the public and internal fields and properties from a type. +/// +internal sealed class Reflector +{ + private readonly HashSet collectedPropertyNames = new(); + private readonly HashSet collectedFields = new(); + private readonly List fields = new(); + private List properties = new(); + + public Reflector(Type typeToReflect, MemberKind kind) + { + LoadProperties(typeToReflect, kind); + LoadFields(typeToReflect, kind); + + Members = properties.Concat(fields).ToArray(); + } + + private void LoadProperties(Type typeToReflect, MemberKind kind) + { + while (typeToReflect != null && typeToReflect != typeof(object)) + { + var allProperties = typeToReflect.GetProperties( + BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); + + AddNormalProperties(kind, allProperties); + + AddExplicitlyImplementedProperties(kind, allProperties); + + AddInterfaceProperties(typeToReflect, kind); + + // Move to the base type + typeToReflect = typeToReflect.BaseType; + } + + properties = properties.Where(x => !x.IsIndexer()).ToList(); + } + + private void AddNormalProperties(MemberKind kind, PropertyInfo[] allProperties) + { + if (kind.HasFlag(MemberKind.Public) || kind.HasFlag(MemberKind.Internal) || + kind.HasFlag(MemberKind.ExplicitlyImplemented)) + { + foreach (var property in allProperties) + { + if (!collectedPropertyNames.Contains(property.Name) && !property.IsExplicitlyImplemented() && + HasVisibility(kind, property)) + { + properties.Add(property); + collectedPropertyNames.Add(property.Name); + } + } + } + } + + private static bool HasVisibility(MemberKind kind, PropertyInfo prop) + { + return (kind.HasFlag(MemberKind.Public) && prop.GetMethod?.IsPublic is true) || + (kind.HasFlag(MemberKind.Internal) && + (prop.GetMethod?.IsAssembly is true || prop.GetMethod?.IsFamilyOrAssembly is true)); + } + + private void AddExplicitlyImplementedProperties(MemberKind kind, PropertyInfo[] allProperties) + { + if (kind.HasFlag(MemberKind.ExplicitlyImplemented)) + { + foreach (var p in allProperties) + { + if (p.IsExplicitlyImplemented()) + { + var name = p.Name.Split('.').Last(); + + if (!collectedPropertyNames.Contains(name)) + { + properties.Add(p); + collectedPropertyNames.Add(name); + } + } + } + } + } + + private void AddInterfaceProperties(Type typeToReflect, MemberKind kind) + { + if (kind.HasFlag(MemberKind.DefaultInterfaceProperties) || typeToReflect.IsInterface) + { + // Add explicitly implemented interface properties (not included above) + var interfaces = typeToReflect.GetInterfaces(); + + foreach (var iface in interfaces) + { + foreach (var prop in iface.GetProperties()) + { + if (!collectedPropertyNames.Contains(prop.Name) && + (!prop.GetMethod.IsAbstract || typeToReflect.IsInterface)) + { + properties.Add(prop); + collectedPropertyNames.Add(prop.Name); + } + } + } + } + } + + private void LoadFields(Type typeToReflect, MemberKind kind) + { + while (typeToReflect != null && typeToReflect != typeof(object)) + { + var files = typeToReflect.GetFields( + BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); + + foreach (var field in files) + { + if (!collectedFields.Contains(field.Name) && HasVisibility(kind, field)) + { + fields.Add(field); + collectedFields.Add(field.Name); + } + } + + // Move to the base type + typeToReflect = typeToReflect.BaseType; + } + } + + private static bool HasVisibility(MemberKind kind, FieldInfo field) + { + return (kind.HasFlag(MemberKind.Public) && field.IsPublic) || + (kind.HasFlag(MemberKind.Internal) && (field.IsAssembly || field.IsFamilyOrAssembly)); + } + + public MemberInfo[] Members { get; } + + public PropertyInfo[] Properties => properties.ToArray(); + + public FieldInfo[] Fields => fields.ToArray(); +} diff --git a/src/Reflectify/Reflectify.csproj b/src/Reflectify/Reflectify.csproj new file mode 100644 index 0000000..376e9b8 --- /dev/null +++ b/src/Reflectify/Reflectify.csproj @@ -0,0 +1,21 @@ + + + + net47;net6.0;netstandard2.0;netstandard2.1 + enable + default + disable + .nuspec + version=$(Version) + REFLECTIFY_COMPILE + + + + bin\Debug\Reflectify.xml + + + + bin\Release\Reflectify.xml + + + diff --git a/src/Reflectify/Reflectify.props b/src/Reflectify/Reflectify.props new file mode 100644 index 0000000..574faca --- /dev/null +++ b/src/Reflectify/Reflectify.props @@ -0,0 +1,7 @@ + + + + false + + + diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..2df66ca --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,42 @@ +[*.cs] + +# Purpose: Do not nest type GetProperties. +# Reason: We don't care about this in tests +dotnet_diagnostic.CA1034.severity = none + +# Purpose: Type is a static holder type but is neither static nor NotInheritable +# Reason: We don't care about this in tests +dotnet_diagnostic.CA1052.severity = none + +# Purpose: Remove the underscores from member name +# Reason: We don't care about this in tests +dotnet_diagnostic.CA1707.severity = none + +# Purpose: Type 'SuperClass' can be sealed +# Reason: We don't care about this in tests +dotnet_diagnostic.CA1852.severity = none + +# Purpose: Elements must be documented +# Reason: Not needed for tests +dotnet_diagnostic.SA1600.severity = none + +# Purpose: Field should be private +# Reason: Not needed for tests +dotnet_diagnostic.SA1401.severity = none + +# Purpose: Make class static +# Reason: Not needed for tests +dotnet_diagnostic.RCS1102.severity = none + +# Purpose: Type contains the word 'and' +# Reason: Not needed for tests +dotnet_diagnostic.AV1000.severity = none + +# Purpose: Type hides inherited member +# Reason: Not needed for tests +dotnet_diagnostic.AV1010.severity = none + + + + + diff --git a/tests/Reflectify.Specs/Reflectify.Specs.csproj b/tests/Reflectify.Specs/Reflectify.Specs.csproj new file mode 100644 index 0000000..0a3fd96 --- /dev/null +++ b/tests/Reflectify.Specs/Reflectify.Specs.csproj @@ -0,0 +1,51 @@ + + + + net6.0;netcoreapp3.0;net472 + default + true + true + false + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/Reflectify.Specs/TypeExtensionsSpecs.cs b/tests/Reflectify.Specs/TypeExtensionsSpecs.cs new file mode 100644 index 0000000..495cdb2 --- /dev/null +++ b/tests/Reflectify.Specs/TypeExtensionsSpecs.cs @@ -0,0 +1,297 @@ +using System.Globalization; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using JetBrains.Annotations; +using Xunit; + +namespace Reflectify.Specs; + +public class TypeExtensionsSpecs +{ + public class GetProperties + { + [Fact] + public void Can_get_all_public_explicit_and_default_interface_properties() + { + // Act + var properties = typeof(SuperClass).GetProperties( + MemberKind.Public | MemberKind.ExplicitlyImplemented | MemberKind.DefaultInterfaceProperties); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "NormalProperty", PropertyType = typeof(string) }, + new { Name = "NewProperty", PropertyType = typeof(int) }, + new { Name = "InterfaceProperty", PropertyType = typeof(string) }, + new + { + Name = $"{typeof(IInterfaceWithSingleProperty).FullName!.Replace("+", ".")}.ExplicitlyImplementedProperty", + PropertyType = typeof(string) + }, +#if NETCOREAPP3_0_OR_GREATER + new { Name = "DefaultProperty", PropertyType = typeof(string) } +#endif + }); + } + + [Fact] + public void Can_get_all_properties_from_an_interface() + { + // Act + var properties = typeof(IInterfaceWithDefaultProperty).GetProperties(MemberKind.Public); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "InterfaceProperty", PropertyType = typeof(string) }, + new { Name = "ExplicitlyImplementedProperty", PropertyType = typeof(string) }, +#if NETCOREAPP3_0_OR_GREATER + new { Name = "DefaultProperty", PropertyType = typeof(string) }, +#endif + }); + } + + [Fact] + public void Can_get_normal_public_properties() + { + // Act + var properties = typeof(SuperClass).GetProperties(MemberKind.Public); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "NormalProperty", PropertyType = typeof(string) }, + new { Name = "NewProperty", PropertyType = typeof(int) }, + new { Name = "InterfaceProperty", PropertyType = typeof(string) } + }); + } + + [Fact] + public void Can_get_explicit_properties_only() + { + // Act + var properties = typeof(SuperClass).GetProperties(MemberKind.ExplicitlyImplemented); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new + { + Name = + $"{typeof(IInterfaceWithSingleProperty).FullName!.Replace("+", ".")}.ExplicitlyImplementedProperty", + PropertyType = typeof(string) + } + }); + } + + [Fact] + public void Prefers_normal_property_over_explicitly_implemented_one() + { + // Act + var properties = typeof(ClassWithExplicitAndNormalProperty).GetProperties( + MemberKind.Public | MemberKind.ExplicitlyImplemented); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new + { + Name = "ExplicitlyImplementedProperty", + PropertyType = typeof(int) + } + }); + } + +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void Can_get_default_interface_properties_only() + { + // Act + var properties = typeof(SuperClass).GetProperties(MemberKind.DefaultInterfaceProperties); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "DefaultProperty", PropertyType = typeof(string) } + }); + } +#endif + + [Fact] + public void Can_get_internal_properties() + { + // Act + var properties = typeof(SuperClass).GetProperties(MemberKind.Internal); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "InternalProperty", PropertyType = typeof(bool) }, + new { Name = "InternalProtectedProperty", PropertyType = typeof(bool) } + }); + } + + [Fact] + public void Will_ignore_indexers() + { + // Act + var properties = typeof(ClassWithIndexer).GetProperties(MemberKind.Public); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "Foo", PropertyType = typeof(object) } + }); + } + } + + public class GetFields + { + [Fact] + public void Can_find_public_fields() + { + // Act + var fields = typeof(SuperClass).GetFields(MemberKind.Public); + + // Assert + fields.Should().BeEquivalentTo(new[] + { + new { Name = "NormalField", FieldType = typeof(string) } + }); + } + + [Fact] + public void Can_find_internal_fields() + { + // Act + var fields = typeof(SuperClass).GetFields(MemberKind.Internal); + + // Assert + fields.Should().BeEquivalentTo(new[] + { + new { Name = "InternalField", FieldType = typeof(string) }, + new { Name = "ProtectedInternalField", FieldType = typeof(string) } + }); + } + + [Fact] + public void Can_find_all_fields() + { + // Act + var fields = typeof(SuperClass).GetFields( + MemberKind.Internal | MemberKind.Public); + + // Assert + fields.Should().BeEquivalentTo(new[] + { + new { Name = "NormalField", FieldType = typeof(string) }, + new { Name = "InternalField", FieldType = typeof(string) }, + new { Name = "ProtectedInternalField", FieldType = typeof(string) } + }); + } + } + + public class GetMembers + { + [Fact] + public void Can_find_all_members() + { + // Act + var members = typeof(SuperClass).GetMembers(MemberKind.Public); + + // Assert + members.Should().BeEquivalentTo([ + new { Name = "NormalProperty" }, + new { Name = "NewProperty" }, + new { Name = "InterfaceProperty" }, + new { Name = "NormalField" } + ]); + } + } + + public class PropertyInfoExtensions + { + [Fact] + public void Can_determine_a_property_is_an_indexer() + { + // Act + var indexer = typeof(ClassWithIndexer).GetProperty("Item"); + + // Assert + indexer.IsIndexer().Should().BeTrue(); + } + + [Fact] + public void Can_determine_a_property_is_not_an_indexer() + { + // Act + var indexer = typeof(ClassWithIndexer).GetProperty("Foo"); + + // Assert + indexer.IsIndexer().Should().BeFalse(); + } + } + + private class SuperClass : BaseClass, IInterfaceWithDefaultProperty + { + public string NormalProperty { get; set; } + + public new int NewProperty { get; set; } + + internal bool InternalProperty { get; set; } + + protected internal bool InternalProtectedProperty { get; set; } + + string IInterfaceWithSingleProperty.ExplicitlyImplementedProperty { get; set; } + + public string InterfaceProperty { get; set; } + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + public string NormalField; + + internal string InternalField; + + protected internal string ProtectedInternalField; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + } + + private sealed class ClassWithExplicitAndNormalProperty : IInterfaceWithSingleProperty + { + string IInterfaceWithSingleProperty.ExplicitlyImplementedProperty { get; set; } + + [UsedImplicitly] + public int ExplicitlyImplementedProperty { get; set; } + } + + private class BaseClass + { + [UsedImplicitly] + public string NewProperty { get; set; } + } + + private interface IInterfaceWithDefaultProperty : IInterfaceWithSingleProperty + { + [UsedImplicitly] + string InterfaceProperty { get; set; } + +#if NETCOREAPP3_0_OR_GREATER + [UsedImplicitly] + string DefaultProperty => "Default"; +#endif + } + + private interface IInterfaceWithSingleProperty + { + [UsedImplicitly] + string ExplicitlyImplementedProperty { get; set; } + } + + private sealed class ClassWithIndexer + { + [UsedImplicitly] + public object Foo { get; set; } + + public string this[int n] => n.ToString(CultureInfo.InvariantCulture); + } +}