├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── pull-request.yaml ├── .gitignore ├── Ben.Demystifier.sln ├── Directory.Build.props ├── LICENSE ├── README.md ├── build.ps1 ├── images └── icon.png ├── sample ├── FSharpStackTrace │ ├── FSharpStackTrace.fsproj │ └── Program.fs └── StackTrace │ ├── Program.cs │ └── StackTrace.csproj ├── src └── Ben.Demystifier │ ├── Ben.Demystifier.csproj │ ├── EnhancedStackFrame.cs │ ├── EnhancedStackTrace.Frames.cs │ ├── EnhancedStackTrace.cs │ ├── Enumerable │ ├── EnumerableIList.cs │ ├── EnumeratorIList.cs │ └── IEnumerableIList.cs │ ├── ExceptionExtensions.cs │ ├── Internal │ ├── ILReader.cs │ ├── PortablePdbReader.cs │ └── ReflectionHelper.cs │ ├── ResolvedMethod.cs │ ├── ResolvedParameter.cs │ ├── StringBuilderExtentions.cs │ ├── TypeNameHelper.cs │ ├── ValueTupleResolvedParameter.cs │ └── key.snk ├── test ├── Ben.Demystifier.Benchmarks │ ├── Ben.Demystifier.Benchmarks.csproj │ ├── Exceptions.cs │ └── Program.cs └── Ben.Demystifier.Test │ ├── AggregateException.cs │ ├── AsyncEnumerableTests.cs │ ├── Ben.Demystifier.Test.csproj │ ├── DynamicCompilation.cs │ ├── EnhancedStackTraceTests.cs │ ├── GenericMethodDisplayStringTests.cs │ ├── ILReaderTests.cs │ ├── InheritenceTests.cs │ ├── LineEndingsHelper.cs │ ├── MethodTests.cs │ ├── MixedStack.cs │ ├── NonThrownException.cs │ ├── ParameterParamTests.cs │ ├── RecursionTests.cs │ ├── ReflectionHelperTests.cs │ ├── ResolvedMethodTests.cs │ ├── ToDemystifiedStringTests.cs │ ├── TuplesTests.cs │ └── TypeNameTests.cs └── version.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # ASP.NET Core EditorConfig file 2 | 3 | # NOTE: This file focuses on settings Visual Studio 2017 supports natively. For example, VS does not support insert_final_newline. 4 | # We do use it, but it's harder to enforce without a separate VS extension or an editor that supports it. 5 | # See https://docs.microsoft.com/en-us/visualstudio/ide/create-portable-custom-editor-options for more 6 | 7 | # Mark this file as the "root" for everything below this point. This means that editor config files above 8 | # this file will be ignored 9 | root = true 10 | 11 | # Default settings 12 | [*] 13 | indent_style = space 14 | indent_size = 4 15 | charset = utf-8 16 | insert_final_newline = true 17 | 18 | # Unix-only files 19 | [*.sh] 20 | end_of_line = lf 21 | 22 | # 2-space files 23 | [{*.json,*.yml}] 24 | indent_size = 2 25 | 26 | # .NET Code Style Settings 27 | # See https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 28 | # REVIEW: Should these be errors? warnings? suggestions? 29 | [{*.cs,*.vb}] 30 | dotnet_sort_system_directives_first = true 31 | 32 | # Don't use 'this.'/'Me.' prefix for anything 33 | dotnet_style_qualification_for_field = false:error 34 | dotnet_style_qualification_for_property = false:error 35 | dotnet_style_qualification_for_method = false:error 36 | dotnet_style_qualification_for_event = false:error 37 | 38 | # Use language keywords over framework type names for type references 39 | # i.e. prefer 'string' over 'String' 40 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 41 | dotnet_style_predefined_type_for_member_access = true:error 42 | 43 | # Prefer object/collection initializers 44 | # This is a suggestion because there are cases where this is necessary 45 | dotnet_style_object_initializer = true:suggestion 46 | dotnet_style_collection_initializer = true:suggestion 47 | 48 | # C# 7: Prefer using named tuple names over '.Item1', '.Item2', etc. 49 | dotnet_style_explicit_tuple_names = true:error 50 | 51 | # Prefer using 'foo ?? bar' over 'foo != null ? foo : bar' 52 | dotnet_style_coalesce_expression = true:error 53 | 54 | # Prefer using '?.' over ternary null checking where possible 55 | dotnet_style_null_propagation = true:error 56 | 57 | # Use 'var' in all cases where it can be used 58 | csharp_style_var_for_built_in_types = true:error 59 | csharp_style_var_when_type_is_apparent = true:error 60 | csharp_style_var_elsewhere = true:error 61 | 62 | # C# 7: Prefer using pattern matching over "if(x is T) { var t = (T)x; }" and "var t = x as T; if(t != null) { ... }" 63 | # REVIEW: Since this is a new C# 7 feature that replaces an existing pattern, I'm making it a suggestion 64 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 65 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 66 | 67 | # C# 7: Prefer using 'out var' where possible 68 | # REVIEW: Since this is a new C# 7 feature that replaces an existing pattern, I'm making it a suggestion 69 | csharp_style_inlined_variable_declaration = true:error 70 | 71 | # C# 7: Use throw expressions when null-checking 72 | # @davidfowl hates them :) 73 | csharp_style_throw_expression = false:error 74 | 75 | # Prefer using "func?.Invoke(args)" over "if(func != null) { func(args); }" 76 | # REVIEW: Maybe an error? 77 | csharp_style_conditional_delegate_call = true:error 78 | 79 | # Newline settings 80 | # Unsure where docs are. Got these from https://github.com/dotnet/roslyn/blob/master/.editorconfig 81 | csharp_new_line_before_open_brace = all 82 | csharp_new_line_before_else = true 83 | csharp_new_line_before_catch = true 84 | csharp_new_line_before_finally = true 85 | csharp_new_line_before_members_in_object_initializers = true 86 | csharp_new_line_before_members_in_anonymous_types = true 87 | 88 | # Prefer expression-bodied methods, constructors, operators, etc. 89 | csharp_style_expression_bodied_methods = true:suggestion 90 | csharp_style_expression_bodied_constructors = true:suggestion 91 | csharp_style_expression_bodied_operators = true:suggestion 92 | csharp_style_expression_bodied_properties = true:suggestion 93 | csharp_style_expression_bodied_indexers = true:suggestion 94 | csharp_style_expression_bodied_accessors = true:suggestion -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | github: [benaadams] 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Demystifier PR Build 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | build: 10 | name: "Build for PR" 11 | runs-on: ${{ matrix.os }} 12 | env: 13 | DOTNET_NOLOGO: true 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [windows-latest, ubuntu-18.04, macOS-latest] 18 | config: [Debug, Release] 19 | steps: 20 | - name: Clone source 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup .NET SDK (v2.1) 26 | uses: actions/setup-dotnet@v1.7.2 27 | with: 28 | dotnet-version: '2.1.818' 29 | - name: Setup .NET SDK (v3.1) 30 | uses: actions/setup-dotnet@v1.7.2 31 | with: 32 | dotnet-version: '3.1.414' 33 | - name: Setup .NET SDK (v5.0) 34 | uses: actions/setup-dotnet@v1.7.2 35 | with: 36 | dotnet-version: '5.0.402' 37 | - name: Setup .NET SDK (v6.0) 38 | uses: actions/setup-dotnet@v1.7.2 39 | with: 40 | dotnet-version: '6.0.100-rc.2.21505.57' 41 | 42 | - name: Get .NET information 43 | run: dotnet --info 44 | 45 | - name: Build 46 | run: dotnet build -c ${{ matrix.config }} 47 | 48 | - name: "Test" 49 | run: dotnet test -c ${{ matrix.config }} 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | BenchmarkDotNet.Artifacts/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # .NET Core 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | **/Properties/launchSettings.json 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignorable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | orleans.codegen.cs 204 | 205 | # Since there are multiple workflows, uncomment next line to ignore bower_components 206 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 207 | #bower_components/ 208 | 209 | # RIA/Silverlight projects 210 | Generated_Code/ 211 | 212 | # Backup & report files from converting an old project file 213 | # to a newer Visual Studio version. Backup files are not needed, 214 | # because we have git ;-) 215 | _UpgradeReport_Files/ 216 | Backup*/ 217 | UpgradeLog*.XML 218 | UpgradeLog*.htm 219 | 220 | # SQL Server files 221 | *.mdf 222 | *.ldf 223 | *.ndf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | node_modules/ 239 | 240 | # Typescript v1 declaration files 241 | typings/ 242 | 243 | # Visual Studio 6 build log 244 | *.plg 245 | 246 | # Visual Studio 6 workspace options file 247 | *.opt 248 | 249 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 250 | *.vbw 251 | 252 | # Visual Studio LightSwitch build output 253 | **/*.HTMLClient/GeneratedArtifacts 254 | **/*.DesktopClient/GeneratedArtifacts 255 | **/*.DesktopClient/ModelManifest.xml 256 | **/*.Server/GeneratedArtifacts 257 | **/*.Server/ModelManifest.xml 258 | _Pvt_Extensions 259 | 260 | # Paket dependency manager 261 | .paket/paket.exe 262 | paket-files/ 263 | 264 | # FAKE - F# Make 265 | .fake/ 266 | 267 | # JetBrains Rider 268 | .idea/ 269 | *.sln.iml 270 | 271 | # CodeRush 272 | .cr/ 273 | 274 | # Python Tools for Visual Studio (PTVS) 275 | __pycache__/ 276 | *.pyc 277 | 278 | # Cake - Uncomment if you are using it 279 | # tools/** 280 | # !tools/packages.config 281 | 282 | # Telerik's JustMock configuration file 283 | *.jmconfig 284 | 285 | # BizTalk build output 286 | *.btp.cs 287 | *.btm.cs 288 | *.odx.cs 289 | *.xsd.cs 290 | -------------------------------------------------------------------------------- /Ben.Demystifier.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27019.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A2FCCAAC-BE90-4F7E-B95F-A72D46DDD6B3}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{59CA6310-4AA5-4093-95D4-472B94DC0CD4}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier", "src\Ben.Demystifier\Ben.Demystifier.csproj", "{5410A056-89AB-4912-BD1E-A63616AD91D0}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier.Test", "test\Ben.Demystifier.Test\Ben.Demystifier.Test.csproj", "{B9E150B0-AEEB-4D98-8BE1-92C1296699A2}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{455921D3-DD54-4355-85CF-F4009DF2AB70}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackTrace", "sample\StackTrace\StackTrace.csproj", "{E161FC12-53C2-47CD-A5FC-3684B86723A9}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5937ACDF-0059-488E-9604-D84689C72933}" 19 | ProjectSection(SolutionItems) = preProject 20 | appveyor.yml = appveyor.yml 21 | build.ps1 = build.ps1 22 | Directory.Build.props = Directory.Build.props 23 | README.md = README.md 24 | version.json = version.json 25 | EndProjectSection 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier.Benchmarks", "test\Ben.Demystifier.Benchmarks\Ben.Demystifier.Benchmarks.csproj", "{EF5557DF-C48E-4999-846C-D99A92E86373}" 28 | EndProject 29 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpStackTrace", "sample\FSharpStackTrace\FSharpStackTrace.fsproj", "{D6B779D2-A678-47CC-A2F9-A312292EA7A2}" 30 | EndProject 31 | Global 32 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 33 | Debug|Any CPU = Debug|Any CPU 34 | Release|Any CPU = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Debug|Any CPU.ActiveCfg = Release|Any CPU 50 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Debug|Any CPU.Build.0 = Release|Any CPU 51 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Release|Any CPU.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(NestedProjects) = preSolution 62 | {5410A056-89AB-4912-BD1E-A63616AD91D0} = {A2FCCAAC-BE90-4F7E-B95F-A72D46DDD6B3} 63 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2} = {59CA6310-4AA5-4093-95D4-472B94DC0CD4} 64 | {E161FC12-53C2-47CD-A5FC-3684B86723A9} = {455921D3-DD54-4355-85CF-F4009DF2AB70} 65 | {EF5557DF-C48E-4999-846C-D99A92E86373} = {59CA6310-4AA5-4093-95D4-472B94DC0CD4} 66 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2} = {455921D3-DD54-4355-85CF-F4009DF2AB70} 67 | EndGlobalSection 68 | GlobalSection(ExtensibilityGlobals) = postSolution 69 | SolutionGuid = {841B7D5F-E810-4F94-A529-002C7E075216} 70 | EndGlobalSection 71 | EndGlobal 72 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | latest 4 | true 5 | 6 | 7 | 8 | 9 | 10 | true 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ben.Demystifier 2 | [![NuGet version (Ben.Demystifier)](https://img.shields.io/nuget/v/Ben.Demystifier.svg?style=flat-square)](https://www.nuget.org/packages/Ben.Demystifier/) 3 | [![build](https://github.com/benaadams/Ben.Demystifier/workflows/Demystifier%20PR%20Build/badge.svg)](https://github.com/benaadams/Ben.Demystifier/actions) 4 | 5 | Output the modern C# 7.0+ features in stack traces that looks like the C# source code that generated them rather than IL formatted. 6 | 7 | ## High performance understanding for stack traces 8 | 9 | .NET stack traces output the compiler transformed methods; rather than the source code methods, which make them slow to mentally parse and match back to the source code. 10 | 11 | The current output was good for C# 1.0; but has become progressively worse since C# 2.0 (iterators, generics) as new features are added to the .NET languages and at C# 7.1 the stack traces are esoteric (see: [Problems with current stack traces](#problems-with-current-stack-traces)). 12 | 13 | ### Make error logs more productive 14 | 15 | Output the modern C# 7.0+ features in stack traces in an understandable fashion that looks like the C# source code that generated them. 16 | 17 | [![Demystified stacktrace](https://aoa.blob.core.windows.net/aspnet/stacktrace-demystified.png)](https://aoa.blob.core.windows.net/aspnet/stacktrace-demystified.png) 18 | 19 | ### Usage 20 | 21 | ``` 22 | exception.Demystify() 23 | ``` 24 | Or instead of Environment.StackTrace 25 | ``` 26 | EnhancedStackTrace.Current() 27 | ``` 28 | Resolves the stack back to the C# source format of the calls (and is an inspectable list of stack frames) 29 | 30 | Calling `.ToString()` on the Demystified exception will produce a string stacktrace similar to the following (without the comments): 31 | 32 | ```csharp 33 | System.InvalidOperationException: Collection was modified; enumeration operation may not execute. 34 | at bool System.Collections.Generic.List+Enumerator.MoveNextRare() 35 | at IEnumerable Program.Iterator(int startAt)+MoveNext() // Resolved enumerator 36 | at bool System.Linq.Enumerable+SelectEnumerableIterator.MoveNext() // Resolved enumerator 37 | at string string.Join(string separator, IEnumerable values) 38 | at string Program+GenericClass.GenericMethod(ref TSubType value) 39 | at async Task Program.MethodAsync(int value) // Resolved async 40 | at async Task Program.MethodAsync(TValue value) // Resolved async 41 | at string Program.Method(string value)+()=>{} [0] // lambda source + ordinal 42 | at string Program.Method(string value)+()=>{} [1] // lambda source + ordinal 43 | at string Program.RunLambda(Func lambda) 44 | at (string val, bool) Program.Method(string value) // Tuple returning 45 | at ref string Program.RefMethod(in string value)+LocalFuncRefReturn() // ref return local func 46 | at int Program.RefMethod(in string value)+LocalFuncParam(string val) // local function 47 | at string Program.RefMethod(in string value) // in param (readonly ref) 48 | at (string val, bool) static Program()+(string s, bool b)=>{} // tuple return static lambda 49 | at void static Program()+(string s, bool b)=>{} // void static lambda 50 | at void Program.Start((string val, bool) param) // Resolved tuple param 51 | at void Program.Start((string val, bool) param)+LocalFunc1(long l) // void local function 52 | at bool Program.Start((string val, bool) param)+LocalFunc2(bool b1, bool b2) // bool return local function 53 | at string Program.Start() 54 | at void Program()+()=>{} // ctor defined lambda 55 | at void Program(Action action)+(object state)=>{} // ctor defined lambda 56 | at void Program.RunAction(Action lambda, object state) 57 | at new Program(Action action) // constructor 58 | at new Program() // constructor 59 | at void Program.Main(String[] args) 60 | ``` 61 | 62 | Calling `.ToString()` on the same exception would produce the following output 63 | 64 | ```csharp 65 | System.InvalidOperationException: Collection was modified; enumeration operation may not execute. 66 | at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() // ? low value 67 | at System.Collections.Generic.List`1.Enumerator.MoveNextRare() 68 | at Program.d__3.MoveNext() // which enumerator? 69 | at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext() // which enumerator? 70 | at System.String.Join(String separator, IEnumerable`1 values) 71 | at Program.GenericClass`1.GenericMethod[TSubType](TSubType& value) 72 | at Program.d__4.MoveNext() // which async overload? 73 | --- End of stack trace from previous location where exception was thrown --- // ? no value 74 | at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // ? no value 75 | at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // ? no value 76 | at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() // ? no value 77 | at Program.d__5`1.MoveNext() // which async overload? 78 | --- End of stack trace from previous location where exception was thrown --- // ? no value 79 | at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // ? no value 80 | at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // ? no value 81 | at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() // ? no value 82 | at Program.<>c__DisplayClass8_0.b__0() // ¯\_(ツ)_/¯ 83 | at Program.<>c__DisplayClass8_0.b__1() // ¯\_(ツ)_/¯ 84 | at Program.RunLambda(Func`1 lambda) 85 | at Program.Method(String value) 86 | at Program.g__LocalFuncRefReturn|10_1(<>c__DisplayClass10_0& ) // local function 87 | at Program.g__LocalFuncParam|10_0(String val, <>c__DisplayClass10_0& ) // local function 88 | at Program.RefMethod(String value) 89 | at Program.<>c.<.cctor>b__18_1(String s, Boolean b) // ¯\_(ツ)_/¯ 90 | at Program.<>c.<.cctor>b__18_0(String s, Boolean b) // ¯\_(ツ)_/¯ 91 | at Program.Start(ValueTuple`2 param) // Tuple param? 92 | at Program.g__LocalFunc1|11_0(Int64 l) // local function 93 | at Program.g__LocalFunc2|11_1(Boolean b1, Boolean b2) // local function 94 | at Program.Start() 95 | at Program.<>c.<.ctor>b__1_0() // ¯\_(ツ)_/¯ 96 | at Program.<>c__DisplayClass2_0.<.ctor>b__0(Object state) // ¯\_(ツ)_/¯ 97 | at Program.RunAction(Action`1 lambda, Object state) 98 | at Program..ctor(Action action) // constructor 99 | at Program..ctor() // constructor 100 | at Program.Main(String[] args) 101 | ``` 102 | Which is far less helpful, and close to jibberish in places 103 | 104 | 105 | ### Problems with current stack traces: 106 | 107 | * **constructors** 108 | 109 | Does not match code, output as `.ctor` and `.cctor` 110 | 111 | * **parameters** 112 | 113 | Do not specify qualifier `ref`, `out` or `in` 114 | 115 | * **iterators** 116 | 117 | Cannot determine overload `d__3.MoveNext()` rather than `Iterator(int startAt)+MoveNext()` 118 | * **Linq** 119 | 120 | Cannot determine overload 121 | 122 | `Linq.Enumerable.SelectEnumerableIterator``2.MoveNext()` 123 | 124 | rather than 125 | 126 | `Linq.Enumerable+SelectEnumerableIterator.MoveNext()` 127 | * **async** 128 | 129 | Cannot determine overload and no modifier such as `async` 130 | 131 | `d__5``1.MoveNext()` 132 | 133 | rather than 134 | 135 | `async Task Program.MethodAsync(int value)` 136 | 137 | Noise! 138 | ``` 139 | --- End of stack trace from previous location where exception was thrown --- 140 | at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() 141 | at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 142 | at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 143 | at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task) 144 | at System.Runtime.CompilerServices.TaskAwaiter.GetResult() 145 | ``` 146 | 147 | * **lambdas** 148 | 149 | Mostly jibberish `<>c__DisplayClass2_0.<.ctor>b__0(Object state)` with a suggestion of where they are declared but no hint if there are multiple overloads of the method. 150 | * **local functions** 151 | 152 | Mostly jibberish `g__LocalFuncParam|10_0(String val, <>c__DisplayClass10_0& )` with a suggestion of where they are declared but no hint if there are multiple overloads of the method. 153 | 154 | * **generic parameters** 155 | 156 | Not resolved, only an indication of the number `RunLambda(Func``1 lambda)` rather than `RunLambda(Func lambda)` 157 | * **value tuples** 158 | 159 | Do not match code, output as `ValueTuple``2 param` rather than `(string val, bool) param` 160 | * **primitive types** 161 | 162 | Do not match code, output as `Int64`, `Boolean`, `String` rather than `long`, `bool`, `string` 163 | * **return types** 164 | 165 | Skipped entirely from method signature 166 | 167 | ### Benchmarks 168 | 169 | To run benchmarks from the repository root: 170 | ``` 171 | dotnet run -p .\test\Ben.Demystifier.Benchmarks\ -c Release -f netcoreapp2.0 All 172 | ``` 173 | Note: we're only kicking off via `netcoreapp2.0`, benchmarks will run for all configured platforms like `net462`. 174 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode 4 | to see if an error occcured. If an error is detected then an exception is thrown. 5 | This function allows you to run command-line programs without having to 6 | explicitly check the $lastexitcode variable. 7 | .EXAMPLE 8 | exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" 9 | #> 10 | function Exec 11 | { 12 | [CmdletBinding()] 13 | param( 14 | [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, 15 | [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) 16 | ) 17 | & $cmd 18 | if ($lastexitcode -ne 0) { 19 | throw ("Exec: " + $errorMessage) 20 | } 21 | } 22 | 23 | if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse } 24 | 25 | exec { & dotnet test .\test\Ben.Demystifier.Test -c Release } 26 | 27 | exec { & dotnet pack .\src\Ben.Demystifier -c Release -o .\artifacts } 28 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benaadams/Ben.Demystifier/8db93654c2869d3bc5ddb1462682f421c99a056b/images/icon.png -------------------------------------------------------------------------------- /sample/FSharpStackTrace/FSharpStackTrace.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1;net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/FSharpStackTrace/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | 3 | open System 4 | open System.Diagnostics 5 | open FSharp.Control.Tasks 6 | 7 | let call i = async { 8 | do! Async.Sleep 1 9 | if i = 10 then 10 | failwith "BOOM!" 11 | return i 12 | } 13 | 14 | let run count = async { 15 | let calls = Array.init count call 16 | for call in calls do 17 | let! _ = call 18 | () 19 | return 0 20 | } 21 | 22 | let makeTheCall () = task { 23 | let! x = run 20 24 | return x 25 | } 26 | 27 | [] 28 | let main argv = 29 | try 30 | let results = makeTheCall().GetAwaiter().GetResult() 31 | printfn "%A" results 32 | with e -> 33 | printfn "%s" <| string (e.Demystify()) 34 | 0 // return an integer exit code 35 | -------------------------------------------------------------------------------- /sample/StackTrace/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.InteropServices; 7 | using System.Threading.Tasks; 8 | 9 | class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | Exception exception = null; 14 | try 15 | { 16 | new Program(); 17 | } 18 | catch (Exception ex) 19 | { 20 | Console.WriteLine(ex); 21 | exception = ex.Demystify(); 22 | } 23 | 24 | Console.WriteLine(); 25 | Console.WriteLine(exception); 26 | } 27 | 28 | static Action s_action = (string s, bool b) => s_func(s, b); 29 | static Func s_func = (string s, bool b) => (RefMethod(s), b); 30 | 31 | Action, object> _action = (Action lambda, object state) => lambda(state); 32 | 33 | static string s = ""; 34 | 35 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 36 | Program() : this(() => Start()) 37 | { 38 | 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 42 | Program(Action action) => RunAction((state) => _action((s) => action(), state), null); 43 | 44 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 45 | static IEnumerable Iterator(int startAt) 46 | { 47 | var list = new List() { 1, 2, 3, 4 }; 48 | foreach (var item in list) 49 | { 50 | // Throws the exception 51 | list.Add(item); 52 | 53 | yield return item.ToString(); 54 | } 55 | } 56 | 57 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 58 | static async Task MethodAsync(int value) 59 | { 60 | await Task.Delay(0); 61 | return GenericClass.GenericMethod(ref value); 62 | } 63 | 64 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 65 | static async Task MethodAsync(TValue value) 66 | { 67 | return await MethodLocalAsync(); 68 | 69 | async Task MethodLocalAsync() 70 | { 71 | return await MethodAsync(1); 72 | } 73 | } 74 | 75 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 76 | static void RunAction(Action lambda, object state) => lambda(state); 77 | 78 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 79 | static string RunLambda(Func lambda) => lambda(); 80 | 81 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 82 | static (string val, bool) Method(string value) 83 | { 84 | #pragma warning disable IDE0039 // Use local function 85 | Func func = () => MethodAsync(value).GetAwaiter().GetResult(); 86 | #pragma warning restore IDE0039 // Use local function 87 | var anonType = new { func }; 88 | return (RunLambda(() => anonType.func()), true); 89 | } 90 | 91 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 92 | static ref string RefMethod(int value) => ref s; 93 | 94 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 95 | static string RefMethod(in string value) 96 | { 97 | var val = value; 98 | return LocalFuncParam(value).ToString(); 99 | 100 | int LocalFuncParam(string s) 101 | { 102 | return int.Parse(LocalFuncRefReturn()); 103 | } 104 | 105 | ref string LocalFuncRefReturn() 106 | { 107 | Method(val); 108 | return ref s; 109 | } 110 | } 111 | 112 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 113 | static string Start() 114 | { 115 | return LocalFunc2(true, false).ToString(); 116 | 117 | void LocalFunc1(long l) 118 | { 119 | Start((val: "", true)); 120 | } 121 | 122 | bool LocalFunc2(bool b1, bool b2) 123 | { 124 | LocalFunc1(1); 125 | return true; 126 | } 127 | } 128 | 129 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 130 | static ref string RefMethod(bool value) => ref s; 131 | 132 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 133 | static void Start((string val, bool) param) => s_action.Invoke(param.val, param.Item2); 134 | 135 | 136 | class GenericClass 137 | { 138 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 139 | public static string GenericMethod(ref TSubType value) 140 | { 141 | var returnVal = ""; 142 | for (var i = 0; i < 10; i++) 143 | { 144 | try 145 | { 146 | returnVal += string.Join(", ", Iterator(5).Select(s => s)); 147 | } 148 | catch (Exception ex) 149 | { 150 | throw new Exception(ex.Message, ex); 151 | } 152 | } 153 | 154 | return returnVal; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /sample/StackTrace/StackTrace.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Ben.Demystifier.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ben Core 5 | Ben.Demystifier 6 | High performance understanding for stack traces (Make error logs more productive) 7 | Ben Adams 8 | https://github.com/benaadams/Ben.Demystifier 9 | https://github.com/benaadams/Ben.Demystifier 10 | Apache-2.0 11 | git 12 | true 13 | embedded 14 | true 15 | enable 16 | true 17 | true 18 | readme.md 19 | 20 | 21 | 22 | netstandard2.1;netstandard2.0;net45;net6.0 23 | true 24 | key.snk 25 | icon.png 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 5.0.0 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | <_Parameter1>$(AssemblyName).Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001005532489e147c0de0c5872048d20f7acf99d172a599d217950eba8fdbd1f98fa5ac47901b076d2bd7da8d436e6b5d6292694902e9748514bb0c3b17e6a0e0386f22447847c1c5cd9e034f79a8fe1c120a12785f7f79617414e63861cf13d6fd1cbb4211b87202c6a52c1e22962a6bd310413c37ca440fad14ab8422707517fbae 43 | 44 | 45 | 46 | 47 | 48 | True 49 | \ 50 | 51 | 52 | True 53 | \ 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/EnhancedStackFrame.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Reflection; 5 | 6 | namespace System.Diagnostics 7 | { 8 | public class EnhancedStackFrame : StackFrame 9 | { 10 | private readonly string? _fileName; 11 | private readonly int _lineNumber; 12 | private readonly int _colNumber; 13 | 14 | public StackFrame StackFrame { get; } 15 | 16 | public bool IsRecursive 17 | { 18 | get => MethodInfo.RecurseCount > 0; 19 | internal set => MethodInfo.RecurseCount++; 20 | } 21 | 22 | public ResolvedMethod MethodInfo { get; } 23 | 24 | internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber) 25 | : base(fileName, lineNumber, colNumber) 26 | { 27 | StackFrame = stackFrame; 28 | MethodInfo = methodInfo; 29 | 30 | _fileName = fileName; 31 | _lineNumber = lineNumber; 32 | _colNumber = colNumber; 33 | } 34 | 35 | internal bool IsEquivalent(ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber) 36 | { 37 | return _lineNumber == lineNumber && 38 | _colNumber == colNumber && 39 | _fileName == fileName && 40 | MethodInfo.IsSequentialEquivalent(methodInfo); 41 | } 42 | 43 | /// 44 | /// Gets the column number in the file that contains the code that is executing. 45 | /// This information is typically extracted from the debugging symbols for the executable. 46 | /// 47 | /// The file column number, or 0 (zero) if the file column number cannot be determined. 48 | public override int GetFileColumnNumber() => _colNumber; 49 | 50 | /// 51 | /// Gets the line number in the file that contains the code that is executing. 52 | /// This information is typically extracted from the debugging symbols for the executable. 53 | /// 54 | /// The file line number, or 0 (zero) if the file line number cannot be determined. 55 | public override int GetFileLineNumber() => _lineNumber; 56 | 57 | /// 58 | /// Gets the file name that contains the code that is executing. 59 | /// This information is typically extracted from the debugging symbols for the executable. 60 | /// 61 | /// The file name, or null if the file name cannot be determined. 62 | public override string? GetFileName() => _fileName; 63 | 64 | /// 65 | /// Gets the offset from the start of the Microsoft intermediate language (MSIL) 66 | /// code for the method that is executing. This offset might be an approximation 67 | /// depending on whether or not the just-in-time (JIT) compiler is generating debugging 68 | /// code. The generation of this debugging information is controlled by the System.Diagnostics.DebuggableAttribute. 69 | /// 70 | /// The offset from the start of the MSIL code for the method that is executing. 71 | public override int GetILOffset() => StackFrame.GetILOffset(); 72 | 73 | /// 74 | /// Gets the method in which the frame is executing. 75 | /// 76 | /// The method in which the frame is executing. 77 | public override MethodBase? GetMethod() => StackFrame.GetMethod(); 78 | 79 | /// 80 | /// Gets the offset from the start of the native just-in-time (JIT)-compiled code 81 | /// for the method that is being executed. The generation of this debugging information 82 | /// is controlled by the System.Diagnostics.DebuggableAttribute class. 83 | /// 84 | /// The offset from the start of the JIT-compiled code for the method that is being executed. 85 | public override int GetNativeOffset() => StackFrame.GetNativeOffset(); 86 | 87 | /// 88 | /// Builds a readable representation of the stack trace. 89 | /// 90 | /// A readable representation of the stack trace. 91 | public override string ToString() => MethodInfo.ToString(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/EnhancedStackTrace.Frames.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | // Copyright (c) .NET Foundation. All rights reserved. 4 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 5 | 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Collections.Generic.Enumerable; 9 | using System.Diagnostics.Internal; 10 | using System.Linq; 11 | using System.Reflection; 12 | using System.Runtime.CompilerServices; 13 | using System.Runtime.ExceptionServices; 14 | using System.Text; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | 18 | namespace System.Diagnostics 19 | { 20 | public partial class EnhancedStackTrace 21 | { 22 | private static readonly Type? StackTraceHiddenAttributeType = Type.GetType("System.Diagnostics.StackTraceHiddenAttribute", false); 23 | private static readonly Type? AsyncIteratorStateMachineAttributeType = Type.GetType("System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", false); 24 | 25 | static EnhancedStackTrace() 26 | { 27 | if (AsyncIteratorStateMachineAttributeType != null) return; 28 | 29 | Assembly mba; 30 | try 31 | { 32 | mba = Assembly.Load("Microsoft.Bcl.AsyncInterfaces"); 33 | } 34 | catch 35 | { 36 | return; 37 | } 38 | 39 | AsyncIteratorStateMachineAttributeType = mba.GetType("System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", false); 40 | } 41 | 42 | private static List GetFrames(Exception exception) 43 | { 44 | if (exception == null) 45 | { 46 | return new List(); 47 | } 48 | 49 | var needFileInfo = true; 50 | var stackTrace = new StackTrace(exception, needFileInfo); 51 | 52 | return GetFrames(stackTrace); 53 | } 54 | 55 | public static List GetFrames(StackTrace stackTrace) 56 | { 57 | var frames = new List(); 58 | var stackFrames = stackTrace.GetFrames(); 59 | 60 | if (stackFrames == null) 61 | { 62 | return frames; 63 | } 64 | 65 | EnhancedStackFrame? lastFrame = null; 66 | PortablePdbReader? portablePdbReader = null; 67 | try 68 | { 69 | for (var i = 0; i < stackFrames.Length; i++) 70 | { 71 | var frame = stackFrames[i]; 72 | if (frame is null) 73 | { 74 | continue; 75 | } 76 | var method = frame.GetMethod(); 77 | 78 | // Always show last stackFrame 79 | if (method != null && !ShowInStackTrace(method) && i < stackFrames.Length - 1) 80 | { 81 | continue; 82 | } 83 | 84 | var fileName = frame.GetFileName(); 85 | var row = frame.GetFileLineNumber(); 86 | var column = frame.GetFileColumnNumber(); 87 | var ilOffset = frame.GetILOffset(); 88 | if (method != null && string.IsNullOrEmpty(fileName) && ilOffset >= 0) 89 | { 90 | // .NET Framework and older versions of mono don't support portable PDBs 91 | // so we read it manually to get file name and line information 92 | (portablePdbReader ??= new PortablePdbReader()).PopulateStackFrame(frame, method, frame.GetILOffset(), out fileName, out row, out column); 93 | } 94 | 95 | if (method is null) 96 | { 97 | // Method can't be null 98 | continue; 99 | } 100 | 101 | var resolvedMethod = GetMethodDisplayString(method); 102 | if (lastFrame?.IsEquivalent(resolvedMethod, fileName, row, column) ?? false) 103 | { 104 | lastFrame.IsRecursive = true; 105 | } 106 | else 107 | { 108 | var stackFrame = new EnhancedStackFrame(frame, resolvedMethod, fileName, row, column); 109 | frames.Add(stackFrame); 110 | lastFrame = stackFrame; 111 | } 112 | } 113 | } 114 | finally 115 | { 116 | portablePdbReader?.Dispose(); 117 | } 118 | 119 | return frames; 120 | } 121 | 122 | public static ResolvedMethod GetMethodDisplayString(MethodBase originMethod) 123 | { 124 | var method = originMethod; 125 | 126 | var methodDisplayInfo = new ResolvedMethod 127 | { 128 | SubMethodBase = method 129 | }; 130 | 131 | // Type name 132 | var type = method.DeclaringType; 133 | 134 | var subMethodName = method.Name; 135 | var methodName = method.Name; 136 | 137 | var isAsyncStateMachine = typeof(IAsyncStateMachine).IsAssignableFrom(type); 138 | if (isAsyncStateMachine || typeof(IEnumerator).IsAssignableFrom(type)) 139 | { 140 | methodDisplayInfo.IsAsync = isAsyncStateMachine; 141 | 142 | // Convert StateMachine methods to correct overload +MoveNext() 143 | if (!TryResolveStateMachineMethod(ref method, out type)) 144 | { 145 | methodDisplayInfo.SubMethodBase = null; 146 | subMethodName = null; 147 | } 148 | 149 | methodName = method.Name; 150 | } 151 | else if (IsFSharpAsync(method)) 152 | { 153 | methodDisplayInfo.IsAsync = true; 154 | methodDisplayInfo.SubMethodBase = null; 155 | subMethodName = null; 156 | methodName = null; 157 | } 158 | 159 | // Method name 160 | methodDisplayInfo.MethodBase = method; 161 | methodDisplayInfo.Name = methodName; 162 | if (method.Name.IndexOf("<") >= 0) 163 | { 164 | if (TryResolveGeneratedName(ref method, out type, out methodName, out subMethodName, out var kind, out var ordinal)) 165 | { 166 | methodName = method.Name; 167 | methodDisplayInfo.MethodBase = method; 168 | methodDisplayInfo.Name = methodName; 169 | methodDisplayInfo.Ordinal = ordinal; 170 | } 171 | else 172 | { 173 | methodDisplayInfo.MethodBase = null; 174 | } 175 | 176 | methodDisplayInfo.IsLambda = (kind == GeneratedNameKind.LambdaMethod); 177 | 178 | if (methodDisplayInfo.IsLambda && type != null) 179 | { 180 | if (methodName == ".cctor") 181 | { 182 | if (type.IsGenericTypeDefinition && !type.IsConstructedGenericType) 183 | { 184 | // TODO: diagnose type's generic type arguments from frame's "this" or something 185 | } 186 | else 187 | { 188 | var fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 189 | foreach (var field in fields) 190 | { 191 | var value = field.GetValue(field); 192 | if (value is Delegate d && d.Target is not null) 193 | { 194 | if (ReferenceEquals(d.Method, originMethod) && 195 | d.Target.ToString() == originMethod.DeclaringType?.ToString()) 196 | { 197 | methodDisplayInfo.Name = field.Name; 198 | methodDisplayInfo.IsLambda = false; 199 | method = originMethod; 200 | break; 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | } 208 | 209 | if (subMethodName != methodName) 210 | { 211 | methodDisplayInfo.SubMethod = subMethodName; 212 | } 213 | 214 | // ResolveStateMachineMethod may have set declaringType to null 215 | if (type != null) 216 | { 217 | methodDisplayInfo.DeclaringType = type; 218 | } 219 | 220 | if (method is MethodInfo mi) 221 | { 222 | var returnParameter = mi.ReturnParameter; 223 | if (returnParameter != null) 224 | { 225 | methodDisplayInfo.ReturnParameter = GetParameter(mi.ReturnParameter); 226 | } 227 | else if (mi.ReturnType != null) 228 | { 229 | methodDisplayInfo.ReturnParameter = new ResolvedParameter(mi.ReturnType) 230 | { 231 | Prefix = "", 232 | Name = "", 233 | }; 234 | } 235 | } 236 | 237 | if (method.IsGenericMethod) 238 | { 239 | var genericArguments = method.GetGenericArguments(); 240 | var genericArgumentsString = string.Join(", ", genericArguments 241 | .Select(arg => TypeNameHelper.GetTypeDisplayName(arg, fullName: false, includeGenericParameterNames: true))); 242 | methodDisplayInfo.GenericArguments += "<" + genericArgumentsString + ">"; 243 | methodDisplayInfo.ResolvedGenericArguments = genericArguments; 244 | } 245 | 246 | // Method parameters 247 | var parameters = method.GetParameters(); 248 | if (parameters.Length > 0) 249 | { 250 | var parameterList = new List(parameters.Length); 251 | foreach (var parameter in parameters) 252 | { 253 | parameterList.Add(GetParameter(parameter)); 254 | } 255 | 256 | methodDisplayInfo.Parameters = parameterList; 257 | } 258 | 259 | if (methodDisplayInfo.SubMethodBase == methodDisplayInfo.MethodBase) 260 | { 261 | methodDisplayInfo.SubMethodBase = null; 262 | } 263 | else if (methodDisplayInfo.SubMethodBase != null) 264 | { 265 | parameters = methodDisplayInfo.SubMethodBase.GetParameters(); 266 | if (parameters.Length > 0) 267 | { 268 | var parameterList = new List(parameters.Length); 269 | foreach (var parameter in parameters) 270 | { 271 | var param = GetParameter(parameter); 272 | if (param.Name?.StartsWith("<") ?? true) continue; 273 | 274 | parameterList.Add(param); 275 | } 276 | 277 | methodDisplayInfo.SubMethodParameters = parameterList; 278 | } 279 | } 280 | 281 | return methodDisplayInfo; 282 | } 283 | 284 | private static bool IsFSharpAsync(MethodBase method) 285 | { 286 | if (method is MethodInfo minfo) 287 | { 288 | var returnType = minfo.ReturnType; 289 | if (returnType.Namespace == "Microsoft.FSharp.Control" && returnType.Name == "FSharpAsync`1") 290 | { 291 | return true; 292 | } 293 | } 294 | 295 | return false; 296 | } 297 | 298 | private static bool TryResolveGeneratedName(ref MethodBase method, out Type? type, out string methodName, out string? subMethodName, out GeneratedNameKind kind, out int? ordinal) 299 | { 300 | kind = GeneratedNameKind.None; 301 | type = method.DeclaringType; 302 | subMethodName = null; 303 | ordinal = null; 304 | methodName = method.Name; 305 | 306 | var generatedName = methodName; 307 | 308 | if (!TryParseGeneratedName(generatedName, out kind, out var openBracketOffset, out var closeBracketOffset)) 309 | { 310 | return false; 311 | } 312 | 313 | methodName = generatedName.Substring(openBracketOffset + 1, closeBracketOffset - openBracketOffset - 1); 314 | 315 | switch (kind) 316 | { 317 | case GeneratedNameKind.LocalFunction: 318 | { 319 | var localNameStart = generatedName.IndexOf((char)kind, closeBracketOffset + 1); 320 | if (localNameStart < 0) break; 321 | localNameStart += 3; 322 | 323 | if (localNameStart < generatedName.Length) 324 | { 325 | var localNameEnd = generatedName.IndexOf("|", localNameStart); 326 | if (localNameEnd > 0) 327 | { 328 | subMethodName = generatedName.Substring(localNameStart, localNameEnd - localNameStart); 329 | } 330 | } 331 | break; 332 | } 333 | case GeneratedNameKind.LambdaMethod: 334 | subMethodName = ""; 335 | break; 336 | } 337 | 338 | var dt = method.DeclaringType; 339 | if (dt == null) 340 | { 341 | return false; 342 | } 343 | 344 | var matchHint = GetMatchHint(kind, method); 345 | 346 | var matchName = methodName; 347 | 348 | var candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 349 | if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true; 350 | 351 | var candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 352 | if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true; 353 | 354 | const int MaxResolveDepth = 10; 355 | for (var i = 0; i < MaxResolveDepth; i++) 356 | { 357 | dt = dt.DeclaringType; 358 | if (dt == null) 359 | { 360 | return false; 361 | } 362 | 363 | candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 364 | if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true; 365 | 366 | candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 367 | if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true; 368 | 369 | if (methodName == ".cctor") 370 | { 371 | candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 372 | foreach (var cctor in candidateConstructors) 373 | { 374 | method = cctor; 375 | type = dt; 376 | return true; 377 | } 378 | } 379 | } 380 | 381 | return false; 382 | } 383 | 384 | private static bool TryResolveSourceMethod(IEnumerable candidateMethods, GeneratedNameKind kind, string? matchHint, ref MethodBase method, ref Type? type, out int? ordinal) 385 | { 386 | ordinal = null; 387 | foreach (var candidateMethod in candidateMethods) 388 | { 389 | if (candidateMethod.GetMethodBody() is not { } methodBody) 390 | { 391 | continue; 392 | } 393 | if (kind == GeneratedNameKind.LambdaMethod) 394 | { 395 | foreach (var v in EnumerableIList.Create(methodBody.LocalVariables)) 396 | { 397 | if (v.LocalType == type) 398 | { 399 | GetOrdinal(method, ref ordinal); 400 | 401 | } 402 | method = candidateMethod; 403 | type = method.DeclaringType; 404 | return true; 405 | } 406 | } 407 | 408 | try 409 | { 410 | var rawIl = methodBody.GetILAsByteArray(); 411 | if (rawIl is null) 412 | { 413 | continue; 414 | } 415 | var reader = new ILReader(rawIl); 416 | while (reader.Read(candidateMethod)) 417 | { 418 | if (reader.Operand is MethodBase mb) 419 | { 420 | if (method == mb || matchHint != null && method.Name.Contains(matchHint)) 421 | { 422 | if (kind == GeneratedNameKind.LambdaMethod) 423 | { 424 | GetOrdinal(method, ref ordinal); 425 | } 426 | 427 | method = candidateMethod; 428 | type = method.DeclaringType; 429 | return true; 430 | } 431 | } 432 | } 433 | } 434 | catch 435 | { 436 | // https://github.com/benaadams/Ben.Demystifier/issues/32 437 | // Skip methods where il can't be interpreted 438 | } 439 | } 440 | 441 | return false; 442 | } 443 | 444 | private static void GetOrdinal(MethodBase method, ref int? ordinal) 445 | { 446 | var lamdaStart = method.Name.IndexOf((char)GeneratedNameKind.LambdaMethod + "__") + 3; 447 | if (lamdaStart > 3) 448 | { 449 | var secondStart = method.Name.IndexOf("_", lamdaStart) + 1; 450 | if (secondStart > 0) 451 | { 452 | lamdaStart = secondStart; 453 | } 454 | 455 | if (!int.TryParse(method.Name.Substring(lamdaStart), out var foundOrdinal)) 456 | { 457 | ordinal = null; 458 | return; 459 | } 460 | 461 | ordinal = foundOrdinal; 462 | 463 | var methods = method.DeclaringType?.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); 464 | 465 | var count = 0; 466 | if (methods != null) 467 | { 468 | var startName = method.Name.Substring(0, lamdaStart); 469 | foreach (var m in methods) 470 | { 471 | if (m.Name.Length > lamdaStart && m.Name.StartsWith(startName)) 472 | { 473 | count++; 474 | 475 | if (count > 1) 476 | { 477 | break; 478 | } 479 | } 480 | } 481 | } 482 | 483 | if (count <= 1) 484 | { 485 | ordinal = null; 486 | } 487 | } 488 | } 489 | 490 | static string? GetMatchHint(GeneratedNameKind kind, MethodBase method) 491 | { 492 | var methodName = method.Name; 493 | 494 | switch (kind) 495 | { 496 | case GeneratedNameKind.LocalFunction: 497 | var start = methodName.IndexOf("|"); 498 | if (start < 1) return null; 499 | var end = methodName.IndexOf("_", start) + 1; 500 | if (end <= start) return null; 501 | 502 | return methodName.Substring(start, end - start); 503 | } 504 | return null; 505 | } 506 | 507 | // Parse the generated name. Returns true for names of the form 508 | // [CS$]<[middle]>c[__[suffix]] where [CS$] is included for certain 509 | // generated names, where [middle] and [__[suffix]] are optional, 510 | // and where c is a single character in [1-9a-z] 511 | // (csharp\LanguageAnalysis\LIB\SpecialName.cpp). 512 | internal static bool TryParseGeneratedName( 513 | string name, 514 | out GeneratedNameKind kind, 515 | out int openBracketOffset, 516 | out int closeBracketOffset) 517 | { 518 | openBracketOffset = -1; 519 | if (name.StartsWith("CS$<", StringComparison.Ordinal)) 520 | { 521 | openBracketOffset = 3; 522 | } 523 | else if (name.StartsWith("<", StringComparison.Ordinal)) 524 | { 525 | openBracketOffset = 0; 526 | } 527 | 528 | if (openBracketOffset >= 0) 529 | { 530 | closeBracketOffset = IndexOfBalancedParenthesis(name, openBracketOffset, '>'); 531 | if (closeBracketOffset >= 0 && closeBracketOffset + 1 < name.Length) 532 | { 533 | int c = name[closeBracketOffset + 1]; 534 | if ((c >= '1' && c <= '9') || (c >= 'a' && c <= 'z')) // Note '0' is not special. 535 | { 536 | kind = (GeneratedNameKind)c; 537 | return true; 538 | } 539 | } 540 | } 541 | 542 | kind = GeneratedNameKind.None; 543 | openBracketOffset = -1; 544 | closeBracketOffset = -1; 545 | return false; 546 | } 547 | 548 | 549 | private static int IndexOfBalancedParenthesis(string str, int openingOffset, char closing) 550 | { 551 | var opening = str[openingOffset]; 552 | 553 | var depth = 1; 554 | for (var i = openingOffset + 1; i < str.Length; i++) 555 | { 556 | var c = str[i]; 557 | if (c == opening) 558 | { 559 | depth++; 560 | } 561 | else if (c == closing) 562 | { 563 | depth--; 564 | if (depth == 0) 565 | { 566 | return i; 567 | } 568 | } 569 | } 570 | 571 | return -1; 572 | } 573 | 574 | private static string GetPrefix(ParameterInfo parameter) 575 | { 576 | if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute), false)) 577 | { 578 | return "params"; 579 | } 580 | 581 | if (parameter.IsOut) 582 | { 583 | return "out"; 584 | } 585 | 586 | if (parameter.IsIn) 587 | { 588 | return "in"; 589 | } 590 | 591 | if (parameter.ParameterType.IsByRef) 592 | { 593 | return "ref"; 594 | } 595 | 596 | return string.Empty; 597 | } 598 | 599 | private static ResolvedParameter GetParameter(ParameterInfo parameter) 600 | { 601 | var prefix = GetPrefix(parameter); 602 | var parameterType = parameter.ParameterType; 603 | 604 | if (parameterType.IsGenericType) 605 | { 606 | var customAttribs = parameter.GetCustomAttributes(inherit: false); 607 | 608 | var tupleNameAttribute = customAttribs.OfType().FirstOrDefault(a => a.IsTupleElementNameAttribute()); 609 | 610 | var tupleNames = tupleNameAttribute?.GetTransformerNames(); 611 | 612 | if (tupleNames?.Count > 0) 613 | { 614 | return GetValueTupleParameter(tupleNames, prefix, parameter.Name, parameterType); 615 | } 616 | } 617 | 618 | if (parameterType.IsByRef && parameterType.GetElementType() is {} elementType) 619 | { 620 | parameterType = elementType; 621 | } 622 | 623 | return new ResolvedParameter(parameterType) 624 | { 625 | Prefix = prefix, 626 | Name = parameter.Name, 627 | IsDynamicType = parameter.IsDefined(typeof(DynamicAttribute), false) 628 | }; 629 | } 630 | 631 | private static ResolvedParameter GetValueTupleParameter(IList tupleNames, string prefix, string? name, Type parameterType) 632 | { 633 | return new ValueTupleResolvedParameter(parameterType, tupleNames) 634 | { 635 | Prefix = prefix, 636 | Name = name, 637 | }; 638 | } 639 | 640 | private static string GetValueTupleParameterName(IList tupleNames, Type parameterType) 641 | { 642 | var sb = new StringBuilder(); 643 | sb.Append("("); 644 | var args = parameterType.GetGenericArguments(); 645 | for (var i = 0; i < args.Length; i++) 646 | { 647 | if (i > 0) 648 | { 649 | sb.Append(", "); 650 | } 651 | 652 | sb.Append(TypeNameHelper.GetTypeDisplayName(args[i], fullName: false, includeGenericParameterNames: true)); 653 | 654 | if (i >= tupleNames.Count) 655 | { 656 | continue; 657 | } 658 | 659 | var argName = tupleNames[i]; 660 | if (argName == null) 661 | { 662 | continue; 663 | } 664 | 665 | sb.Append(" "); 666 | sb.Append(argName); 667 | } 668 | 669 | sb.Append(")"); 670 | return sb.ToString(); 671 | } 672 | 673 | private static bool ShowInStackTrace(MethodBase method) 674 | { 675 | // Since .NET 5: 676 | // https://github.com/dotnet/runtime/blob/7c18d4d6488dab82124d475d1199def01d1d252c/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L348-L361 677 | if ((method.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0) 678 | { 679 | // Aggressive Inlines won't normally show in the StackTrace; however for Tier0 Jit and 680 | // cross-assembly AoT/R2R these inlines will be blocked until Tier1 Jit re-Jits 681 | // them when they will inline. We don't show them in the StackTrace to bring consistency 682 | // between this first-pass asm and fully optimized asm. 683 | return false; 684 | } 685 | 686 | // Since .NET Core 2: 687 | if (StackTraceHiddenAttributeType != null) 688 | { 689 | // Don't show any methods marked with the StackTraceHiddenAttribute 690 | // https://github.com/dotnet/coreclr/pull/14652 691 | if (IsStackTraceHidden(method)) 692 | { 693 | return false; 694 | } 695 | } 696 | 697 | var type = method.DeclaringType; 698 | 699 | if (type == null) 700 | { 701 | return true; 702 | } 703 | 704 | // Since .NET Core 2: 705 | if (StackTraceHiddenAttributeType != null) 706 | { 707 | // Don't show any methods marked with the StackTraceHiddenAttribute 708 | // https://github.com/dotnet/coreclr/pull/14652 709 | if (IsStackTraceHidden(type)) 710 | { 711 | return false; 712 | } 713 | } 714 | 715 | if (type == typeof(Task<>) && method.Name == "InnerInvoke") 716 | { 717 | return false; 718 | } 719 | if (type == typeof(ValueTask<>) && method.Name == "get_Result") 720 | { 721 | return false; 722 | } 723 | if (method.Name.StartsWith("System.Threading.Tasks.Sources.IValueTaskSource") && method.Name.EndsWith(".GetResult")) 724 | { 725 | return false; 726 | } 727 | if (method.Name == "GetResult" && method.DeclaringType?.FullName == "System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore`1") 728 | { 729 | return false; 730 | } 731 | if (type == typeof(Task) || type.DeclaringType == typeof(Task)) 732 | { 733 | if (method.Name.Contains(".cctor")) 734 | { 735 | return false; 736 | } 737 | 738 | switch (method.Name) 739 | { 740 | case "ExecuteWithThreadLocal": 741 | case "Execute": 742 | case "ExecutionContextCallback": 743 | case "ExecuteEntry": 744 | case "InnerInvoke": 745 | case "ExecuteEntryUnsafe": 746 | case "ExecuteFromThreadPool": 747 | return false; 748 | } 749 | } 750 | if (type == typeof(ExecutionContext)) 751 | { 752 | if (method.Name.Contains(".cctor")) 753 | { 754 | return false; 755 | } 756 | 757 | switch (method.Name) 758 | { 759 | case "RunInternal": 760 | case "Run": 761 | case "RunFromThreadPoolDispatchLoop": 762 | return false; 763 | } 764 | } 765 | 766 | if (type.Namespace == "Microsoft.FSharp.Control") 767 | { 768 | switch (type.Name) 769 | { 770 | case "AsyncPrimitives": 771 | case "Trampoline": 772 | return false; 773 | case var typeName when type.IsGenericType: 774 | { 775 | if (typeName == "AsyncResult`1") return false; 776 | else break; 777 | } 778 | } 779 | } 780 | 781 | if (type.Namespace == "Ply") 782 | { 783 | if (type.DeclaringType?.Name == "TplPrimitives") 784 | { 785 | return false; 786 | } 787 | } 788 | 789 | // Fallbacks for runtime pre-StackTraceHiddenAttribute 790 | if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw") 791 | { 792 | return false; 793 | } 794 | 795 | if (type == typeof(TaskAwaiter) || 796 | type == typeof(TaskAwaiter<>) || 797 | type == typeof(ValueTaskAwaiter) || 798 | type == typeof(ValueTaskAwaiter<>) || 799 | type == typeof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter) || 800 | type == typeof(ConfiguredValueTaskAwaitable<>.ConfiguredValueTaskAwaiter) || 801 | type == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || 802 | type == typeof(ConfiguredTaskAwaitable<>.ConfiguredTaskAwaiter)) 803 | { 804 | switch (method.Name) 805 | { 806 | case "HandleNonSuccessAndDebuggerNotification": 807 | case "ThrowForNonSuccess": 808 | case "ValidateEnd": 809 | case "GetResult": 810 | return false; 811 | } 812 | } 813 | else if (type.FullName == "System.ThrowHelper") 814 | { 815 | return false; 816 | } 817 | 818 | return true; 819 | } 820 | 821 | private static bool IsStackTraceHidden(MemberInfo memberInfo) 822 | { 823 | if (StackTraceHiddenAttributeType is not null && !memberInfo.Module.Assembly.ReflectionOnly) 824 | { 825 | return memberInfo.GetCustomAttributes(StackTraceHiddenAttributeType, false).Length != 0; 826 | } 827 | 828 | EnumerableIList attributes; 829 | try 830 | { 831 | attributes = EnumerableIList.Create(memberInfo.GetCustomAttributesData()); 832 | } 833 | catch (NotImplementedException) 834 | { 835 | return false; 836 | } 837 | 838 | foreach (var attribute in attributes) 839 | { 840 | // reflection-only attribute, match on name 841 | if (attribute.AttributeType.FullName == StackTraceHiddenAttributeType?.FullName) 842 | { 843 | return true; 844 | } 845 | } 846 | 847 | return false; 848 | } 849 | 850 | // https://github.com/dotnet/runtime/blob/c985bdcec2a9190e733bcada413a193d5ff60c0d/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L375-L430 851 | private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType) 852 | { 853 | if (method.DeclaringType is null) 854 | { 855 | declaringType = null!; 856 | return false; 857 | } 858 | declaringType = method.DeclaringType; 859 | 860 | var parentType = declaringType.DeclaringType; 861 | if (parentType is null) 862 | { 863 | return false; 864 | } 865 | 866 | static MethodInfo[] GetDeclaredMethods(Type type) => 867 | type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); 868 | 869 | var methods = GetDeclaredMethods(parentType); 870 | if (methods == null) 871 | { 872 | return false; 873 | } 874 | 875 | foreach (var candidateMethod in methods) 876 | { 877 | var attributes = candidateMethod.GetCustomAttributes(inherit: false); 878 | // ReSharper disable once ConditionIsAlwaysTrueOrFalse - Taken from CoreFX 879 | if (attributes is null) 880 | { 881 | continue; 882 | } 883 | 884 | bool foundAttribute = false, foundIteratorAttribute = false; 885 | foreach (var asma in attributes) 886 | { 887 | if (asma.StateMachineType == declaringType) 888 | { 889 | foundAttribute = true; 890 | foundIteratorAttribute |= asma is IteratorStateMachineAttribute 891 | || AsyncIteratorStateMachineAttributeType != null 892 | && AsyncIteratorStateMachineAttributeType.IsInstanceOfType(asma); 893 | } 894 | } 895 | 896 | if (foundAttribute) 897 | { 898 | // If this is an iterator (sync or async), mark the iterator as changed, so it gets the + annotation 899 | // of the original method. Non-iterator async state machines resolve directly to their builder methods 900 | // so aren't marked as changed. 901 | method = candidateMethod; 902 | declaringType = candidateMethod.DeclaringType!; 903 | return foundIteratorAttribute; 904 | } 905 | } 906 | return false; 907 | } 908 | 909 | internal enum GeneratedNameKind 910 | { 911 | None = 0, 912 | 913 | // Used by EE: 914 | ThisProxyField = '4', 915 | HoistedLocalField = '5', 916 | DisplayClassLocalOrField = '8', 917 | LambdaMethod = 'b', 918 | LambdaDisplayClass = 'c', 919 | StateMachineType = 'd', 920 | LocalFunction = 'g', // note collision with Deprecated_InitializerLocal, however this one is only used for method names 921 | 922 | // Used by EnC: 923 | AwaiterField = 'u', 924 | HoistedSynthesizedLocalField = 's', 925 | 926 | // Currently not parsed: 927 | StateMachineStateField = '1', 928 | IteratorCurrentBackingField = '2', 929 | StateMachineParameterProxyField = '3', 930 | ReusableHoistedLocalField = '7', 931 | LambdaCacheField = '9', 932 | FixedBufferField = 'e', 933 | AnonymousType = 'f', 934 | TransparentIdentifier = 'h', 935 | AnonymousTypeField = 'i', 936 | AutoPropertyBackingField = 'k', 937 | IteratorCurrentThreadIdField = 'l', 938 | IteratorFinallyMethod = 'm', 939 | BaseMethodWrapper = 'n', 940 | AsyncBuilderField = 't', 941 | DynamicCallSiteContainerType = 'o', 942 | DynamicCallSiteField = 'p' 943 | } 944 | } 945 | } 946 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/EnhancedStackTrace.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Collections.Generic.Enumerable; 7 | using System.IO; 8 | using System.Text; 9 | 10 | namespace System.Diagnostics 11 | { 12 | public partial class EnhancedStackTrace : StackTrace, IEnumerable 13 | { 14 | public static EnhancedStackTrace Current() => new EnhancedStackTrace(new StackTrace(1 /* skip this one frame */, true)); 15 | 16 | private readonly List _frames; 17 | 18 | // Summary: 19 | // Initializes a new instance of the System.Diagnostics.StackTrace class using the 20 | // provided exception object. 21 | // 22 | // Parameters: 23 | // e: 24 | // The exception object from which to construct the stack trace. 25 | // 26 | // Exceptions: 27 | // T:System.ArgumentNullException: 28 | // The parameter e is null. 29 | public EnhancedStackTrace(Exception e) 30 | { 31 | if (e == null) 32 | { 33 | throw new ArgumentNullException(nameof(e)); 34 | } 35 | 36 | _frames = GetFrames(e); 37 | } 38 | 39 | 40 | public EnhancedStackTrace(StackTrace stackTrace) 41 | { 42 | if (stackTrace == null) 43 | { 44 | throw new ArgumentNullException(nameof(stackTrace)); 45 | } 46 | 47 | _frames = GetFrames(stackTrace); 48 | } 49 | 50 | /// 51 | /// Gets the number of frames in the stack trace. 52 | /// 53 | /// The number of frames in the stack trace. 54 | public override int FrameCount => _frames.Count; 55 | 56 | /// 57 | /// Gets the specified stack frame. 58 | /// 59 | /// The index of the stack frame requested. 60 | /// The specified stack frame. 61 | public override StackFrame GetFrame(int index) => _frames[index]; 62 | 63 | /// 64 | /// Returns a copy of all stack frames in the current stack trace. 65 | /// 66 | /// 67 | /// An array of type System.Diagnostics.StackFrame representing the function calls 68 | /// in the stack trace. 69 | /// 70 | public override StackFrame[] GetFrames() => _frames.ToArray(); 71 | 72 | /// 73 | /// Builds a readable representation of the stack trace. 74 | /// 75 | /// A readable representation of the stack trace. 76 | public override string ToString() 77 | { 78 | if (_frames == null || _frames.Count == 0) return ""; 79 | 80 | var sb = new StringBuilder(); 81 | 82 | Append(sb); 83 | 84 | return sb.ToString(); 85 | } 86 | 87 | 88 | internal void Append(StringBuilder sb) 89 | { 90 | var frames = _frames; 91 | var count = frames.Count; 92 | 93 | for (var i = 0; i < count; i++) 94 | { 95 | if (i > 0) 96 | { 97 | sb.Append(Environment.NewLine); 98 | } 99 | 100 | var frame = frames[i]; 101 | 102 | sb.Append(" at "); 103 | frame.MethodInfo.Append(sb); 104 | 105 | if (frame.GetFileName() is {} fileName 106 | // IsNullOrEmpty alone wasn't enough to disable the null warning 107 | && !string.IsNullOrEmpty(fileName)) 108 | { 109 | sb.Append(" in "); 110 | sb.Append(TryGetFullPath(fileName)); 111 | 112 | } 113 | 114 | var lineNo = frame.GetFileLineNumber(); 115 | if (lineNo != 0) 116 | { 117 | sb.Append(":line "); 118 | sb.Append(lineNo); 119 | } 120 | } 121 | } 122 | 123 | EnumerableIList GetEnumerator() => EnumerableIList.Create(_frames); 124 | IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); 125 | IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); 126 | 127 | /// 128 | /// Tries to convert a given to a full path. 129 | /// Returns original value if the conversion isn't possible or a given path is relative. 130 | /// 131 | public static string TryGetFullPath(string filePath) 132 | { 133 | if (Uri.TryCreate(filePath, UriKind.Absolute, out var uri) && uri.IsFile) 134 | { 135 | return Uri.UnescapeDataString(uri.AbsolutePath); 136 | } 137 | 138 | return filePath; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Enumerable/EnumerableIList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace System.Collections.Generic.Enumerable 5 | { 6 | public static class EnumerableIList 7 | { 8 | public static EnumerableIList Create(IList list) => new EnumerableIList(list); 9 | } 10 | 11 | public struct EnumerableIList : IEnumerableIList, IList 12 | { 13 | private readonly IList _list; 14 | 15 | public EnumerableIList(IList list) => _list = list; 16 | 17 | public EnumeratorIList GetEnumerator() => new EnumeratorIList(_list); 18 | 19 | public static implicit operator EnumerableIList(List list) => new EnumerableIList(list); 20 | 21 | public static implicit operator EnumerableIList(T[] array) => new EnumerableIList(array); 22 | 23 | public static EnumerableIList Empty = default; 24 | 25 | 26 | // IList pass through 27 | 28 | /// 29 | public T this[int index] { get => _list[index]; set => _list[index] = value; } 30 | 31 | /// 32 | public int Count => _list.Count; 33 | 34 | /// 35 | public bool IsReadOnly => _list.IsReadOnly; 36 | 37 | /// 38 | public void Add(T item) => _list.Add(item); 39 | 40 | /// 41 | public void Clear() => _list.Clear(); 42 | 43 | /// 44 | public bool Contains(T item) => _list.Contains(item); 45 | 46 | /// 47 | public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); 48 | 49 | /// 50 | public int IndexOf(T item) => _list.IndexOf(item); 51 | 52 | /// 53 | public void Insert(int index, T item) => _list.Insert(index, item); 54 | 55 | /// 56 | public bool Remove(T item) => _list.Remove(item); 57 | 58 | /// 59 | public void RemoveAt(int index) => _list.RemoveAt(index); 60 | 61 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 62 | 63 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Enumerable/EnumeratorIList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace System.Collections.Generic.Enumerable 5 | { 6 | public struct EnumeratorIList : IEnumerator 7 | { 8 | private readonly IList _list; 9 | private int _index; 10 | 11 | public EnumeratorIList(IList list) 12 | { 13 | _index = -1; 14 | _list = list; 15 | } 16 | 17 | public T Current => _list[_index]; 18 | 19 | public bool MoveNext() 20 | { 21 | _index++; 22 | 23 | return _index < (_list?.Count ?? 0); 24 | } 25 | 26 | public void Dispose() { } 27 | object? IEnumerator.Current => Current; 28 | public void Reset() => _index = -1; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Enumerable/IEnumerableIList.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace System.Collections.Generic.Enumerable 5 | { 6 | interface IEnumerableIList : IEnumerable 7 | { 8 | new EnumeratorIList GetEnumerator(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/ExceptionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Collections.Generic.Enumerable; 6 | using System.Reflection; 7 | using System.Text; 8 | 9 | namespace System.Diagnostics 10 | { 11 | public static class ExceptionExtensions 12 | { 13 | private static readonly FieldInfo? stackTraceString = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic); 14 | 15 | private static void SetStackTracesString(this Exception exception, string value) 16 | => stackTraceString?.SetValue(exception, value); 17 | 18 | /// 19 | /// Demystifies the given and tracks the original stack traces for the whole exception tree. 20 | /// 21 | public static T Demystify(this T exception) where T : Exception 22 | { 23 | try 24 | { 25 | var stackTrace = new EnhancedStackTrace(exception); 26 | 27 | if (stackTrace.FrameCount > 0) 28 | { 29 | exception.SetStackTracesString(stackTrace.ToString()); 30 | } 31 | 32 | if (exception is AggregateException aggEx) 33 | { 34 | foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions)) 35 | { 36 | ex.Demystify(); 37 | } 38 | } 39 | 40 | exception.InnerException?.Demystify(); 41 | } 42 | catch 43 | { 44 | // Processing exceptions shouldn't throw exceptions; if it fails 45 | } 46 | 47 | return exception; 48 | } 49 | 50 | /// 51 | /// Gets demystified string representation of the . 52 | /// 53 | /// 54 | /// method mutates the exception instance that can cause 55 | /// issues if a system relies on the stack trace be in the specific form. 56 | /// Unlike this method is pure. It calls first, 57 | /// computes a demystified string representation and then restores the original state of the exception back. 58 | /// 59 | [Contracts.Pure] 60 | public static string ToStringDemystified(this Exception exception) 61 | => new StringBuilder().AppendDemystified(exception).ToString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Internal/ILReader.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | 4 | namespace System.Diagnostics.Internal 5 | { 6 | internal class ILReader 7 | { 8 | private static OpCode[] singleByteOpCode; 9 | private static OpCode[] doubleByteOpCode; 10 | 11 | private readonly byte[] _cil; 12 | private int ptr; 13 | 14 | 15 | public ILReader(byte[] cil) => _cil = cil; 16 | 17 | public OpCode OpCode { get; private set; } 18 | public int MetadataToken { get; private set; } 19 | public MemberInfo? Operand { get; private set; } 20 | 21 | public bool Read(MethodBase methodInfo) 22 | { 23 | if (ptr < _cil.Length) 24 | { 25 | OpCode = ReadOpCode(); 26 | Operand = ReadOperand(OpCode, methodInfo); 27 | return true; 28 | } 29 | return false; 30 | } 31 | 32 | OpCode ReadOpCode() 33 | { 34 | var instruction = ReadByte(); 35 | if (instruction < 254) 36 | return singleByteOpCode[instruction]; 37 | else 38 | return doubleByteOpCode[ReadByte()]; 39 | } 40 | 41 | MemberInfo? ReadOperand(OpCode code, MethodBase methodInfo) 42 | { 43 | MetadataToken = 0; 44 | int inlineLength; 45 | switch (code.OperandType) 46 | { 47 | case OperandType.InlineMethod: 48 | MetadataToken = ReadInt(); 49 | Type[]? methodArgs = null; 50 | if (methodInfo.GetType() != typeof(ConstructorInfo) && !methodInfo.GetType().IsSubclassOf(typeof(ConstructorInfo))) 51 | { 52 | methodArgs = methodInfo.GetGenericArguments(); 53 | } 54 | Type[]? typeArgs = null; 55 | if (methodInfo.DeclaringType != null) 56 | { 57 | typeArgs = methodInfo.DeclaringType.GetGenericArguments(); 58 | } 59 | try 60 | { 61 | return methodInfo.Module.ResolveMember(MetadataToken, typeArgs, methodArgs); 62 | } 63 | catch 64 | { 65 | // Can return System.ArgumentException : Token xxx is not a valid MemberInfo token in the scope of module xxx.dll 66 | return null; 67 | } 68 | 69 | case OperandType.InlineNone: 70 | inlineLength = 0; 71 | break; 72 | 73 | case OperandType.ShortInlineBrTarget: 74 | case OperandType.ShortInlineVar: 75 | case OperandType.ShortInlineI: 76 | inlineLength = 1; 77 | break; 78 | 79 | case OperandType.InlineVar: 80 | inlineLength = 2; 81 | break; 82 | 83 | case OperandType.InlineBrTarget: 84 | case OperandType.InlineField: 85 | case OperandType.InlineI: 86 | case OperandType.InlineString: 87 | case OperandType.InlineSig: 88 | case OperandType.InlineSwitch: 89 | case OperandType.InlineTok: 90 | case OperandType.InlineType: 91 | case OperandType.ShortInlineR: 92 | inlineLength = 4; 93 | break; 94 | 95 | case OperandType.InlineI8: 96 | case OperandType.InlineR: 97 | inlineLength = 8; 98 | break; 99 | 100 | default: 101 | return null; 102 | } 103 | 104 | for (var i = 0; i < inlineLength; i++) 105 | { 106 | ReadByte(); 107 | } 108 | 109 | return null; 110 | } 111 | 112 | byte ReadByte() => _cil[ptr++]; 113 | 114 | int ReadInt() 115 | { 116 | var b1 = ReadByte(); 117 | var b2 = ReadByte(); 118 | var b3 = ReadByte(); 119 | var b4 = ReadByte(); 120 | return b1 | b2 << 8 | b3 << 16 | b4 << 24; 121 | } 122 | 123 | static ILReader() 124 | { 125 | singleByteOpCode = new OpCode[225]; 126 | doubleByteOpCode = new OpCode[31]; 127 | 128 | var fields = GetOpCodeFields(); 129 | 130 | for (var i = 0; i < fields.Length; i++) 131 | { 132 | var code = (OpCode)fields[i].GetValue(null)!; 133 | if (code.OpCodeType == OpCodeType.Nternal) 134 | continue; 135 | 136 | if (code.Size == 1) 137 | singleByteOpCode[code.Value] = code; 138 | else 139 | doubleByteOpCode[code.Value & 0xff] = code; 140 | } 141 | } 142 | 143 | static FieldInfo[] GetOpCodeFields() => typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Internal/PortablePdbReader.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Reflection; 7 | using System.Reflection.Metadata; 8 | using System.Reflection.Metadata.Ecma335; 9 | using System.Reflection.PortableExecutable; 10 | 11 | namespace System.Diagnostics.Internal 12 | { 13 | // Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.StackTrace.Sources/StackFrame/PortablePdbReader.cs 14 | internal class PortablePdbReader : IDisposable 15 | { 16 | private readonly Dictionary _cache = 17 | new Dictionary(StringComparer.Ordinal); 18 | 19 | public void PopulateStackFrame(StackFrame frameInfo, MethodBase method, int IlOffset, out string fileName, out int row, out int column) 20 | { 21 | fileName = ""; 22 | row = 0; 23 | column = 0; 24 | 25 | if (method.Module.Assembly.IsDynamic) 26 | { 27 | return; 28 | } 29 | 30 | var metadataReader = GetMetadataReader(method.Module.Assembly.Location); 31 | 32 | if (metadataReader == null) 33 | { 34 | return; 35 | } 36 | 37 | var methodToken = MetadataTokens.Handle(method.MetadataToken); 38 | 39 | Debug.Assert(methodToken.Kind == HandleKind.MethodDefinition); 40 | 41 | var handle = ((MethodDefinitionHandle)methodToken).ToDebugInformationHandle(); 42 | 43 | if (!handle.IsNil) 44 | { 45 | var methodDebugInfo = metadataReader.GetMethodDebugInformation(handle); 46 | var sequencePoints = methodDebugInfo.GetSequencePoints(); 47 | SequencePoint? bestPointSoFar = null; 48 | 49 | foreach (var point in sequencePoints) 50 | { 51 | if (point.Offset > IlOffset) 52 | { 53 | break; 54 | } 55 | 56 | if (point.StartLine != SequencePoint.HiddenLine) 57 | { 58 | bestPointSoFar = point; 59 | } 60 | } 61 | 62 | if (bestPointSoFar.HasValue) 63 | { 64 | row = bestPointSoFar.Value.StartLine; 65 | column = bestPointSoFar.Value.StartColumn; 66 | fileName = metadataReader.GetString(metadataReader.GetDocument(bestPointSoFar.Value.Document).Name); 67 | } 68 | } 69 | } 70 | 71 | private MetadataReader? GetMetadataReader(string assemblyPath) 72 | { 73 | if (!_cache.TryGetValue(assemblyPath, out var provider) && provider is not null) 74 | { 75 | var pdbPath = GetPdbPath(assemblyPath); 76 | 77 | if (!string.IsNullOrEmpty(pdbPath) && File.Exists(pdbPath) && IsPortable(pdbPath!)) 78 | { 79 | var pdbStream = File.OpenRead(pdbPath); 80 | provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); 81 | } 82 | 83 | _cache[assemblyPath] = provider; 84 | } 85 | 86 | return provider?.GetMetadataReader(); 87 | } 88 | 89 | private static string? GetPdbPath(string assemblyPath) 90 | { 91 | if (string.IsNullOrEmpty(assemblyPath)) 92 | { 93 | return null; 94 | } 95 | 96 | if (File.Exists(assemblyPath)) 97 | { 98 | var peStream = File.OpenRead(assemblyPath); 99 | 100 | using var peReader = new PEReader(peStream); 101 | foreach (var entry in peReader.ReadDebugDirectory()) 102 | { 103 | if (entry.Type == DebugDirectoryEntryType.CodeView) 104 | { 105 | var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); 106 | var peDirectory = Path.GetDirectoryName(assemblyPath); 107 | return peDirectory is null 108 | ? null 109 | : Path.Combine(peDirectory, Path.GetFileName(codeViewData.Path)); 110 | } 111 | } 112 | } 113 | 114 | return null; 115 | } 116 | 117 | private static bool IsPortable(string pdbPath) 118 | { 119 | using var pdbStream = File.OpenRead(pdbPath); 120 | return pdbStream.ReadByte() == 'B' && 121 | pdbStream.ReadByte() == 'S' && 122 | pdbStream.ReadByte() == 'J' && 123 | pdbStream.ReadByte() == 'B'; 124 | } 125 | 126 | public void Dispose() 127 | { 128 | foreach (var entry in _cache) 129 | { 130 | entry.Value?.Dispose(); 131 | } 132 | 133 | _cache.Clear(); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Internal/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | using System.Threading; 7 | 8 | namespace System.Diagnostics.Internal 9 | { 10 | /// 11 | /// A helper class that contains utilities methods for dealing with reflection. 12 | /// 13 | public static class ReflectionHelper 14 | { 15 | private static PropertyInfo? transformerNamesLazyPropertyInfo; 16 | 17 | /// 18 | /// Returns true if the is a value tuple type. 19 | /// 20 | public static bool IsValueTuple(this Type type) 21 | { 22 | return type.Namespace == "System" && type.Name.Contains("ValueTuple`"); 23 | } 24 | 25 | /// 26 | /// Returns true if the given is of type TupleElementNameAttribute. 27 | /// 28 | /// 29 | /// To avoid compile-time dependency hell with System.ValueTuple, this method uses reflection and not checks statically that 30 | /// the given is TupleElementNameAttribute. 31 | /// 32 | public static bool IsTupleElementNameAttribute(this Attribute attribute) 33 | { 34 | var attributeType = attribute.GetType(); 35 | return attributeType.Namespace == "System.Runtime.CompilerServices" && 36 | attributeType.Name == "TupleElementNamesAttribute"; 37 | } 38 | 39 | /// 40 | /// Returns 'TransformNames' property value from a given . 41 | /// 42 | /// 43 | /// To avoid compile-time dependency hell with System.ValueTuple, this method uses reflection 44 | /// instead of casting the attribute to a specific type. 45 | /// 46 | public static IList? GetTransformerNames(this Attribute attribute) 47 | { 48 | Debug.Assert(attribute.IsTupleElementNameAttribute()); 49 | 50 | var propertyInfo = GetTransformNamesPropertyInfo(attribute.GetType()); 51 | return propertyInfo?.GetValue(attribute) as IList; 52 | } 53 | 54 | private static PropertyInfo? GetTransformNamesPropertyInfo(Type attributeType) 55 | { 56 | #pragma warning disable 8634 57 | return LazyInitializer.EnsureInitialized(ref transformerNamesLazyPropertyInfo, 58 | #pragma warning restore 8634 59 | () => attributeType.GetProperty("TransformNames", BindingFlags.Instance | BindingFlags.Public)!); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/ResolvedMethod.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic.Enumerable; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace System.Diagnostics 9 | { 10 | public class ResolvedMethod 11 | { 12 | public MethodBase? MethodBase { get; set; } 13 | 14 | public Type? DeclaringType { get; set; } 15 | 16 | public bool IsAsync { get; set; } 17 | 18 | public bool IsLambda { get; set; } 19 | 20 | public ResolvedParameter? ReturnParameter { get; set; } 21 | 22 | public string? Name { get; set; } 23 | 24 | public int? Ordinal { get; set; } 25 | 26 | public string? GenericArguments { get; set; } 27 | 28 | public Type[]? ResolvedGenericArguments { get; set; } 29 | 30 | public MethodBase? SubMethodBase { get; set; } 31 | 32 | public string? SubMethod { get; set; } 33 | 34 | public EnumerableIList Parameters { get; set; } 35 | 36 | public EnumerableIList SubMethodParameters { get; set; } 37 | public int RecurseCount { get; internal set; } 38 | 39 | internal bool IsSequentialEquivalent(ResolvedMethod obj) 40 | { 41 | return 42 | IsAsync == obj.IsAsync && 43 | DeclaringType == obj.DeclaringType && 44 | Name == obj.Name && 45 | IsLambda == obj.IsLambda && 46 | Ordinal == obj.Ordinal && 47 | GenericArguments == obj.GenericArguments && 48 | SubMethod == obj.SubMethod; 49 | } 50 | 51 | public override string ToString() => Append(new StringBuilder()).ToString(); 52 | 53 | public StringBuilder Append(StringBuilder builder) 54 | => Append(builder, true); 55 | 56 | public StringBuilder Append(StringBuilder builder, bool fullName) 57 | { 58 | if (IsAsync) 59 | { 60 | builder.Append("async "); 61 | } 62 | 63 | if (ReturnParameter != null) 64 | { 65 | ReturnParameter.Append(builder); 66 | builder.Append(" "); 67 | } 68 | 69 | if (DeclaringType != null) 70 | { 71 | 72 | if (Name == ".ctor") 73 | { 74 | if (string.IsNullOrEmpty(SubMethod) && !IsLambda) 75 | builder.Append("new "); 76 | 77 | AppendDeclaringTypeName(builder, fullName); 78 | } 79 | else if (Name == ".cctor") 80 | { 81 | builder.Append("static "); 82 | AppendDeclaringTypeName(builder, fullName); 83 | } 84 | else 85 | { 86 | AppendDeclaringTypeName(builder, fullName) 87 | .Append(".") 88 | .Append(Name); 89 | } 90 | } 91 | else 92 | { 93 | builder.Append(Name); 94 | } 95 | builder.Append(GenericArguments); 96 | 97 | builder.Append("("); 98 | if (MethodBase != null) 99 | { 100 | var isFirst = true; 101 | foreach(var param in Parameters) 102 | { 103 | if (isFirst) 104 | { 105 | isFirst = false; 106 | } 107 | else 108 | { 109 | builder.Append(", "); 110 | } 111 | param.Append(builder); 112 | } 113 | } 114 | else 115 | { 116 | builder.Append("?"); 117 | } 118 | builder.Append(")"); 119 | 120 | if (!string.IsNullOrEmpty(SubMethod) || IsLambda) 121 | { 122 | builder.Append("+"); 123 | builder.Append(SubMethod); 124 | builder.Append("("); 125 | if (SubMethodBase != null) 126 | { 127 | var isFirst = true; 128 | foreach (var param in SubMethodParameters) 129 | { 130 | if (isFirst) 131 | { 132 | isFirst = false; 133 | } 134 | else 135 | { 136 | builder.Append(", "); 137 | } 138 | param.Append(builder); 139 | } 140 | } 141 | else 142 | { 143 | builder.Append("?"); 144 | } 145 | builder.Append(")"); 146 | if (IsLambda) 147 | { 148 | builder.Append(" => { }"); 149 | 150 | if (Ordinal.HasValue) 151 | { 152 | builder.Append(" ["); 153 | builder.Append(Ordinal); 154 | builder.Append("]"); 155 | } 156 | } 157 | } 158 | 159 | if (RecurseCount > 0) 160 | { 161 | builder.Append($" x {RecurseCount + 1:0}"); 162 | } 163 | 164 | return builder; 165 | } 166 | 167 | private StringBuilder AppendDeclaringTypeName(StringBuilder builder, bool fullName = true) 168 | { 169 | return DeclaringType != null ? builder.AppendTypeDisplayName(DeclaringType, fullName: fullName, includeGenericParameterNames: true) : builder; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/ResolvedParameter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Text; 5 | 6 | namespace System.Diagnostics 7 | { 8 | public class ResolvedParameter 9 | { 10 | public string? Name { get; set; } 11 | 12 | public Type ResolvedType { get; set; } 13 | 14 | public string? Prefix { get; set; } 15 | public bool IsDynamicType { get; set; } 16 | 17 | public ResolvedParameter(Type resolvedType) => ResolvedType = resolvedType; 18 | 19 | public override string ToString() => Append(new StringBuilder()).ToString(); 20 | 21 | public StringBuilder Append(StringBuilder sb) 22 | { 23 | if (ResolvedType.Assembly.ManifestModule.Name == "FSharp.Core.dll" && ResolvedType.Name == "Unit") 24 | return sb; 25 | 26 | if (!string.IsNullOrEmpty(Prefix)) 27 | { 28 | sb.Append(Prefix) 29 | .Append(" "); 30 | } 31 | 32 | if (IsDynamicType) 33 | { 34 | sb.Append("dynamic"); 35 | } 36 | else if (ResolvedType != null) 37 | { 38 | AppendTypeName(sb); 39 | } 40 | else 41 | { 42 | sb.Append("?"); 43 | } 44 | 45 | if (!string.IsNullOrEmpty(Name)) 46 | { 47 | sb.Append(" ") 48 | .Append(Name); 49 | } 50 | 51 | return sb; 52 | } 53 | 54 | protected virtual void AppendTypeName(StringBuilder sb) 55 | { 56 | sb.AppendTypeDisplayName(ResolvedType, fullName: false, includeGenericParameterNames: true); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/StringBuilderExtentions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic.Enumerable; 5 | using System.Text; 6 | 7 | namespace System.Diagnostics 8 | { 9 | public static class StringBuilderExtentions 10 | { 11 | public static StringBuilder AppendDemystified(this StringBuilder builder, Exception exception) 12 | { 13 | try 14 | { 15 | var stackTrace = new EnhancedStackTrace(exception); 16 | 17 | builder.Append(exception.GetType()); 18 | if (!string.IsNullOrEmpty(exception.Message)) 19 | { 20 | builder.Append(": ").Append(exception.Message); 21 | } 22 | builder.Append(Environment.NewLine); 23 | 24 | if (stackTrace.FrameCount > 0) 25 | { 26 | stackTrace.Append(builder); 27 | } 28 | 29 | if (exception is AggregateException aggEx) 30 | { 31 | foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions)) 32 | { 33 | builder.AppendInnerException(ex); 34 | } 35 | } 36 | 37 | if (exception.InnerException != null) 38 | { 39 | builder.AppendInnerException(exception.InnerException); 40 | } 41 | } 42 | catch 43 | { 44 | // Processing exceptions shouldn't throw exceptions; if it fails 45 | } 46 | 47 | return builder; 48 | } 49 | 50 | private static void AppendInnerException(this StringBuilder builder, Exception exception) 51 | => builder.Append(" ---> ") 52 | .AppendDemystified(exception) 53 | .AppendLine() 54 | .Append(" --- End of inner exception stack trace ---"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/TypeNameHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace System.Diagnostics 8 | { 9 | // Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.TypeNameHelper.Sources/TypeNameHelper.cs 10 | public static class TypeNameHelper 11 | { 12 | public static readonly Dictionary BuiltInTypeNames = new Dictionary 13 | { 14 | { typeof(void), "void" }, 15 | { typeof(bool), "bool" }, 16 | { typeof(byte), "byte" }, 17 | { typeof(char), "char" }, 18 | { typeof(decimal), "decimal" }, 19 | { typeof(double), "double" }, 20 | { typeof(float), "float" }, 21 | { typeof(int), "int" }, 22 | { typeof(long), "long" }, 23 | { typeof(object), "object" }, 24 | { typeof(sbyte), "sbyte" }, 25 | { typeof(short), "short" }, 26 | { typeof(string), "string" }, 27 | { typeof(uint), "uint" }, 28 | { typeof(ulong), "ulong" }, 29 | { typeof(ushort), "ushort" } 30 | }; 31 | 32 | public static readonly Dictionary FSharpTypeNames = new Dictionary 33 | { 34 | { "Unit", "void" }, 35 | { "FSharpOption", "Option" }, 36 | { "FSharpAsync", "Async" }, 37 | { "FSharpOption`1", "Option" }, 38 | { "FSharpAsync`1", "Async" } 39 | }; 40 | 41 | /// 42 | /// Pretty print a type name. 43 | /// 44 | /// The . 45 | /// true to print a fully qualified name. 46 | /// true to include generic parameter names. 47 | /// The pretty printed type name. 48 | public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false) 49 | { 50 | var builder = new StringBuilder(); 51 | ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames)); 52 | return builder.ToString(); 53 | } 54 | 55 | public static StringBuilder AppendTypeDisplayName(this StringBuilder builder, Type type, bool fullName = true, bool includeGenericParameterNames = false) 56 | { 57 | ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames)); 58 | return builder; 59 | } 60 | 61 | /// 62 | /// Returns a name of given generic type without '`'. 63 | /// 64 | public static string GetTypeNameForGenericType(Type type) 65 | { 66 | if (!type.IsGenericType) 67 | { 68 | throw new ArgumentException("The given type should be generic", nameof(type)); 69 | } 70 | 71 | var genericPartIndex = type.Name.IndexOf('`'); 72 | 73 | return (genericPartIndex >= 0) ? type.Name.Substring(0, genericPartIndex) : type.Name; 74 | } 75 | 76 | private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options) 77 | { 78 | if (type.IsGenericType) 79 | { 80 | var underlyingType = Nullable.GetUnderlyingType(type); 81 | if (underlyingType != null) 82 | { 83 | ProcessType(builder, underlyingType, options); 84 | builder.Append('?'); 85 | } 86 | else 87 | { 88 | var genericArguments = type.GetGenericArguments(); 89 | ProcessGenericType(builder, type, genericArguments, genericArguments.Length, options); 90 | } 91 | } 92 | else if (type.IsArray) 93 | { 94 | ProcessArrayType(builder, type, options); 95 | } 96 | else if (BuiltInTypeNames.TryGetValue(type, out var builtInName)) 97 | { 98 | builder.Append(builtInName); 99 | } 100 | else if (type.Namespace == nameof(System)) 101 | { 102 | builder.Append(type.Name); 103 | } 104 | else if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll" 105 | && FSharpTypeNames.TryGetValue(type.Name, out builtInName)) 106 | { 107 | builder.Append(builtInName); 108 | } 109 | else if (type.IsGenericParameter) 110 | { 111 | if (options.IncludeGenericParameterNames) 112 | { 113 | builder.Append(type.Name); 114 | } 115 | } 116 | else 117 | { 118 | builder.Append(options.FullName ? type.FullName ?? type.Name : type.Name); 119 | } 120 | } 121 | 122 | private static void ProcessArrayType(StringBuilder builder, Type type, DisplayNameOptions options) 123 | { 124 | var innerType = type; 125 | while (innerType.IsArray) 126 | { 127 | if (innerType.GetElementType() is { } inner) 128 | { 129 | innerType = inner; 130 | } 131 | } 132 | 133 | ProcessType(builder, innerType, options); 134 | 135 | while (type.IsArray) 136 | { 137 | builder.Append('['); 138 | builder.Append(',', type.GetArrayRank() - 1); 139 | builder.Append(']'); 140 | if (type.GetElementType() is not { } elementType) 141 | { 142 | break; 143 | } 144 | type = elementType; 145 | } 146 | } 147 | 148 | private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length, DisplayNameOptions options) 149 | { 150 | var offset = 0; 151 | if (type.IsNested && type.DeclaringType is not null) 152 | { 153 | offset = type.DeclaringType.GetGenericArguments().Length; 154 | } 155 | 156 | if (options.FullName) 157 | { 158 | if (type.IsNested && type.DeclaringType is not null) 159 | { 160 | ProcessGenericType(builder, type.DeclaringType, genericArguments, offset, options); 161 | builder.Append('+'); 162 | } 163 | else if (!string.IsNullOrEmpty(type.Namespace)) 164 | { 165 | builder.Append(type.Namespace); 166 | builder.Append('.'); 167 | } 168 | } 169 | 170 | var genericPartIndex = type.Name.IndexOf('`'); 171 | if (genericPartIndex <= 0) 172 | { 173 | builder.Append(type.Name); 174 | return; 175 | } 176 | 177 | if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll" 178 | && FSharpTypeNames.TryGetValue(type.Name, out var builtInName)) 179 | { 180 | builder.Append(builtInName); 181 | } 182 | else 183 | { 184 | builder.Append(type.Name, 0, genericPartIndex); 185 | } 186 | 187 | builder.Append('<'); 188 | for (var i = offset; i < length; i++) 189 | { 190 | ProcessType(builder, genericArguments[i], options); 191 | if (i + 1 == length) 192 | { 193 | continue; 194 | } 195 | 196 | builder.Append(','); 197 | if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter) 198 | { 199 | builder.Append(' '); 200 | } 201 | } 202 | builder.Append('>'); 203 | } 204 | 205 | private struct DisplayNameOptions 206 | { 207 | public DisplayNameOptions(bool fullName, bool includeGenericParameterNames) 208 | { 209 | FullName = fullName; 210 | IncludeGenericParameterNames = includeGenericParameterNames; 211 | } 212 | 213 | public bool FullName { get; } 214 | 215 | public bool IncludeGenericParameterNames { get; } 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/ValueTupleResolvedParameter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Ben A Adams. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Diagnostics.Internal; 6 | using System.Text; 7 | 8 | namespace System.Diagnostics 9 | { 10 | public class ValueTupleResolvedParameter : ResolvedParameter 11 | { 12 | public IList TupleNames { get; } 13 | 14 | public ValueTupleResolvedParameter(Type resolvedType, IList tupleNames) 15 | : base(resolvedType) 16 | => TupleNames = tupleNames; 17 | 18 | protected override void AppendTypeName(StringBuilder sb) 19 | { 20 | if (ResolvedType is not null) 21 | { 22 | if (ResolvedType.IsValueTuple()) 23 | { 24 | AppendValueTupleParameterName(sb, ResolvedType); 25 | } 26 | else 27 | { 28 | // Need to unwrap the first generic argument first. 29 | sb.Append(TypeNameHelper.GetTypeNameForGenericType(ResolvedType)); 30 | sb.Append("<"); 31 | AppendValueTupleParameterName(sb, ResolvedType.GetGenericArguments()[0]); 32 | sb.Append(">"); 33 | } 34 | } 35 | } 36 | 37 | private void AppendValueTupleParameterName(StringBuilder sb, Type parameterType) 38 | { 39 | sb.Append("("); 40 | var args = parameterType.GetGenericArguments(); 41 | for (var i = 0; i < args.Length; i++) 42 | { 43 | if (i > 0) 44 | { 45 | sb.Append(", "); 46 | } 47 | 48 | sb.AppendTypeDisplayName(args[i], fullName: false, includeGenericParameterNames: true); 49 | 50 | if (i >= TupleNames.Count) 51 | { 52 | continue; 53 | } 54 | 55 | var argName = TupleNames[i]; 56 | if (argName == null) 57 | { 58 | continue; 59 | } 60 | 61 | sb.Append(" "); 62 | sb.Append(argName); 63 | } 64 | 65 | sb.Append(")"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benaadams/Ben.Demystifier/8db93654c2869d3bc5ddb1462682f421c99a056b/src/Ben.Demystifier/key.snk -------------------------------------------------------------------------------- /test/Ben.Demystifier.Benchmarks/Ben.Demystifier.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.1;netcoreapp3.1;net462;net5.0 4 | Release 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Benchmarks/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Jobs; 6 | 7 | namespace Ben.Demystifier.Benchmarks 8 | { 9 | [SimpleJob(RuntimeMoniker.Net48)] 10 | [SimpleJob(RuntimeMoniker.NetCoreApp21)] 11 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 12 | [SimpleJob(RuntimeMoniker.NetCoreApp50)] 13 | [Config(typeof(Config))] 14 | public class ExceptionTests 15 | { 16 | [Benchmark(Baseline = true, Description = ".ToString()")] 17 | public string Baseline() => new Exception().ToString(); 18 | 19 | [Benchmark(Description = "Demystify().ToString()")] 20 | public string Demystify() => new Exception().Demystify().ToString(); 21 | 22 | [Benchmark(Description = "(left, right).ToString()")] 23 | public string ToStringForTupleBased() => GetException(() => ReturnsTuple()).ToString(); 24 | 25 | [Benchmark(Description = "(left, right).Demystify().ToString()")] 26 | public string ToDemystifyForTupleBased() => GetException(() => ReturnsTuple()).Demystify().ToString(); 27 | 28 | private static Exception GetException(Action action) 29 | { 30 | try 31 | { 32 | action(); 33 | throw new InvalidOperationException("Should not be reachable."); 34 | } 35 | catch (Exception e) 36 | { 37 | return e; 38 | } 39 | } 40 | 41 | private static List<(int left, int right)> ReturnsTuple() => throw new Exception(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Diagnosers; 3 | using BenchmarkDotNet.Running; 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace Ben.Demystifier.Benchmarks 9 | { 10 | public static class Program 11 | { 12 | private const string BenchmarkSuffix = "Tests"; 13 | 14 | public static void Main(string[] args) 15 | { 16 | var benchmarks = Assembly.GetEntryAssembly() 17 | .DefinedTypes.Where(t => t.Name.EndsWith(BenchmarkSuffix)) 18 | .ToDictionary(t => t.Name.Substring(0, t.Name.Length - BenchmarkSuffix.Length), t => t, StringComparer.OrdinalIgnoreCase); 19 | 20 | if (args.Length > 0 && args[0].Equals("all", StringComparison.OrdinalIgnoreCase)) 21 | { 22 | Console.WriteLine("Running full benchmarks suite"); 23 | benchmarks.Select(pair => pair.Value).ToList().ForEach(action => BenchmarkRunner.Run(action)); 24 | return; 25 | } 26 | 27 | if (args.Length == 0 || !benchmarks.ContainsKey(args[0])) 28 | { 29 | Console.WriteLine("Please, select benchmark, list of available:"); 30 | benchmarks 31 | .Select(pair => pair.Key) 32 | .ToList() 33 | .ForEach(Console.WriteLine); 34 | Console.WriteLine("All"); 35 | return; 36 | } 37 | 38 | BenchmarkRunner.Run(benchmarks[args[0]]); 39 | 40 | Console.Read(); 41 | } 42 | } 43 | 44 | internal class Config : ManualConfig 45 | { 46 | public Config() => AddDiagnoser(MemoryDiagnoser.Default); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/AggregateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Ben.Demystifier.Test 10 | { 11 | public class AggregateException 12 | { 13 | [Fact] 14 | public void DemystifiesAggregateExceptions() 15 | { 16 | Exception demystifiedException = null; 17 | 18 | try 19 | { 20 | var tasks = new List 21 | { 22 | Task.Run(async () => await Throw1()), 23 | Task.Run(async () => await Throw2()), 24 | Task.Run(async () => await Throw3()) 25 | }; 26 | 27 | Task.WaitAll(tasks.ToArray()); 28 | } 29 | catch (Exception ex) 30 | { 31 | demystifiedException = ex.Demystify(); 32 | } 33 | 34 | // Assert 35 | var stackTrace = demystifiedException.ToString(); 36 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 37 | var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) 38 | // Remove items that vary between test runners 39 | .Where(s => 40 | (s != " at Task Ben.Demystifier.Test.DynamicCompilation.DoesNotPreventStackTrace()+() => { }" && 41 | !s.Contains("System.Threading.Tasks.Task.WaitAll")) 42 | ) 43 | .Skip(1) 44 | .ToArray()) 45 | // Remove Full framework back arrow 46 | .Replace("<---", ""); 47 | #if NETCOREAPP3_0_OR_GREATER 48 | var expected = string.Join("", new[] { 49 | " ---> System.ArgumentException: Value does not fall within the expected range.", 50 | " at async Task Ben.Demystifier.Test.AggregateException.Throw1()", 51 | " at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }", 52 | " --- End of inner exception stack trace ---", 53 | " at void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()", 54 | " ---> (Inner Exception #1) System.NullReferenceException: Object reference not set to an instance of an object.", 55 | " at async Task Ben.Demystifier.Test.AggregateException.Throw2()", 56 | " at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }", 57 | " ---> (Inner Exception #2) System.InvalidOperationException: Operation is not valid due to the current state of the object.", 58 | " at async Task Ben.Demystifier.Test.AggregateException.Throw3()", 59 | " at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }" 60 | }); 61 | #else 62 | var expected = string.Join("", new[] { 63 | " at async Task Ben.Demystifier.Test.AggregateException.Throw1()", 64 | " at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }", 65 | " --- End of inner exception stack trace ---", 66 | " at void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()", 67 | "---> (Inner Exception #0) System.ArgumentException: Value does not fall within the expected range.", 68 | " at async Task Ben.Demystifier.Test.AggregateException.Throw1()", 69 | " at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }", 70 | "---> (Inner Exception #1) System.NullReferenceException: Object reference not set to an instance of an object.", 71 | " at async Task Ben.Demystifier.Test.AggregateException.Throw2()", 72 | " at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }", 73 | "---> (Inner Exception #2) System.InvalidOperationException: Operation is not valid due to the current state of the object.", 74 | " at async Task Ben.Demystifier.Test.AggregateException.Throw3()", 75 | " at async void Ben.Demystifier.Test.AggregateException.DemystifiesAggregateExceptions()+(?) => { }"}); 76 | #endif 77 | Assert.Equal(expected, trace); 78 | } 79 | 80 | async Task Throw1() 81 | { 82 | await Task.Delay(1).ConfigureAwait(false); 83 | throw new ArgumentException(); 84 | } 85 | 86 | async Task Throw2() 87 | { 88 | await Task.Delay(1).ConfigureAwait(false); 89 | throw new NullReferenceException(); 90 | } 91 | 92 | async Task Throw3() 93 | { 94 | await Task.Delay(1).ConfigureAwait(false); 95 | throw new InvalidOperationException(); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/AsyncEnumerableTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text.RegularExpressions; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace Ben.Demystifier.Test 12 | { 13 | public class AsyncEnumerableTests 14 | { 15 | [Fact] 16 | public async Task DemystifiesAsyncEnumerable() 17 | { 18 | Exception demystifiedException = null; 19 | 20 | try 21 | { 22 | await foreach (var val in Start(CancellationToken.None)) 23 | { 24 | _ = val; 25 | } 26 | } 27 | catch (Exception ex) 28 | { 29 | demystifiedException = ex.Demystify(); 30 | } 31 | 32 | // Assert 33 | var stackTrace = demystifiedException.ToString(); 34 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 35 | var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) 36 | .Select(s => Regex.Replace(s, " x [0-9]+", " x N")) 37 | .Skip(1) 38 | .ToArray()); 39 | var expected = string.Join("", new[] { 40 | " at async IAsyncEnumerable Ben.Demystifier.Test.AsyncEnumerableTests.Throw(CancellationToken cancellationToken)+MoveNext()", 41 | " at async IAsyncEnumerable Ben.Demystifier.Test.AsyncEnumerableTests.Start(CancellationToken cancellationToken)+MoveNext() x N", 42 | " at async Task Ben.Demystifier.Test.AsyncEnumerableTests.DemystifiesAsyncEnumerable() x N" 43 | }); 44 | Assert.Equal(expected, trace); 45 | } 46 | 47 | async IAsyncEnumerable Start([EnumeratorCancellation] CancellationToken cancellationToken) 48 | { 49 | await Task.Delay(1, cancellationToken).ConfigureAwait(false); 50 | yield return 1; 51 | await foreach (var @throw in Throw(cancellationToken)) 52 | { 53 | yield return @throw; 54 | } 55 | } 56 | 57 | async IAsyncEnumerable Throw([EnumeratorCancellation] CancellationToken cancellationToken) 58 | { 59 | yield return 2; 60 | await Task.Delay(1, cancellationToken).ConfigureAwait(false); 61 | throw new InvalidOperationException(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 5 | $(TargetFrameworks);net461 6 | true 7 | ..\..\src\Ben.Demystifier\key.snk 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/DynamicCompilation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Ben.Demystifier.Test 10 | { 11 | public class DynamicCompilation 12 | { 13 | [Fact] 14 | public async Task DoesNotPreventStackTrace() 15 | { 16 | // Arrange 17 | var expression = Expression.Throw( 18 | Expression.New( 19 | typeof(ArgumentException).GetConstructor( 20 | new Type[] {typeof(string)}), 21 | Expression.Constant( "Message"))); 22 | 23 | var lambda = Expression.Lambda(expression); 24 | 25 | var action = lambda.Compile(); 26 | 27 | // Act 28 | Exception demystifiedException = null; 29 | try 30 | { 31 | await Task.Run(() => action()).ConfigureAwait(false); 32 | } 33 | catch(Exception ex) 34 | { 35 | demystifiedException = ex.Demystify(); 36 | } 37 | 38 | // Assert 39 | var stackTrace = demystifiedException.ToString(); 40 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 41 | var trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None) 42 | // Remove items that vary between test runners 43 | .Where(s => 44 | s != " at void System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object state)" && 45 | s != " at Task Ben.Demystifier.Test.DynamicCompilation.DoesNotPreventStackTrace()+() => { }" 46 | ) 47 | .Select(s => Regex.Replace(s, "lambda_method[0-9]+\\(", "lambda_method(")) 48 | .ToArray(); 49 | 50 | Assert.Equal( 51 | new[] { 52 | "System.ArgumentException: Message", 53 | " at void lambda_method(Closure)", 54 | " at async Task Ben.Demystifier.Test.DynamicCompilation.DoesNotPreventStackTrace()"}, 55 | trace); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/EnhancedStackTraceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Xunit; 3 | 4 | namespace Ben.Demystifier.Test 5 | { 6 | public class EnhancedStackTraceTests 7 | { 8 | [Theory] 9 | [InlineData(@"file://Sources\MySolution\Foo.cs", @"/MySolution/Foo.cs")] 10 | [InlineData(@"d:\Public\Src\Foo.cs", @"d:/Public/Src/Foo.cs")] 11 | // To be deterministic, the C# compiler can take a /pathmap command line option. 12 | // This option force the compiler to emit the same bits even when their built from the 13 | // differrent locations. 14 | // The binaries built with the pathmap usually don't have an absolute path, 15 | // but have some prefix like \.\. 16 | // This test case makes sure that EhancedStackTrace can deal with such kind of paths. 17 | [InlineData(@"\.\Public\Src\Foo.cs", @"/./Public/Src/Foo.cs")] 18 | public void RelativePathIsConvertedToAnAbsolutePath(string original, string expected) 19 | { 20 | var converted = EnhancedStackTrace.TryGetFullPath(original); 21 | Assert.Equal(expected, NormalizePath(converted)); 22 | } 23 | 24 | [Theory] 25 | [InlineData(@"file://Sources\My 100%.Done+Solution\Foo`1.cs", @"/My 100%.Done+Solution/Foo`1.cs", false)] 26 | [InlineData(@"d:\Public Files+50%.Done\Src\Foo`1.cs", @"d:/Public Files+50%.Done/Src/Foo`1.cs", false)] 27 | [InlineData(@"\.\Public Files+50%.Done\Src\Foo`1.cs", @"/./Public Files+50%.Done/Src/Foo`1.cs", true)] 28 | public void SpecialPathCharactersAreHandledCorrectly(string original, string expected, bool normalize) 29 | { 30 | var converted = EnhancedStackTrace.TryGetFullPath(original); 31 | if (normalize) 32 | { 33 | converted = NormalizePath(converted); 34 | } 35 | 36 | Assert.Equal(expected, converted); 37 | } 38 | 39 | // Used in tests to avoid platform-specific issues. 40 | private static string NormalizePath(string path) 41 | => path.Replace("\\", "/"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/GenericMethodDisplayStringTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Ben.Demystifier.Test 7 | { 8 | public class GenericMethodDisplayStringTests 9 | { 10 | private static class Example 11 | { 12 | // ReSharper disable once StaticMemberInGenericType 13 | public static readonly StackFrame StackFrame; 14 | 15 | static Example() 16 | { 17 | var fun = new Func(() => new StackFrame(0, true)); 18 | 19 | StackFrame = fun(); 20 | 21 | } 22 | 23 | } 24 | 25 | [Fact] 26 | public void DiagnosesGenericMethodDisplayString() 27 | { 28 | var sf = Example.StackFrame; 29 | 30 | try 31 | { 32 | var s = EnhancedStackTrace.GetMethodDisplayString(sf.GetMethod()); 33 | Assert.True(true, "Does not throw exception when diagnosing generic method display string."); 34 | } 35 | catch (Exception) 36 | { 37 | Assert.True(false, "Must not throw an exception when diagnosing generic method display string."); 38 | } 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/ILReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Internal; 2 | 3 | using Xunit; 4 | 5 | namespace Ben.Demystifier.Test 6 | { 7 | public class ILReaderTests 8 | { 9 | public static TheoryData InlineCilSamples => 10 | new TheoryData 11 | { 12 | // https://github.com/benaadams/Ben.Demystifier/issues/56#issuecomment-366490463 13 | { new byte[] { 14 | 2, 123, 209, 5, 0, 4, 20, 254, 1, 114, 193, 103, 1, 112, 40, 160, 22, 0, 6, 15 | 2, 115, 183, 10, 0, 10, 125, 210, 5, 0, 4, 2, 123, 212, 5, 0, 4, 2, 123, 211, 16 | 5, 0, 4, 40, 221, 15, 0, 6, 44, 68, 2, 123, 212, 5, 0, 4, 111, 103, 17, 0, 6, 2, 17 | 111, 222, 9, 0, 6, 2, 40, 184, 10, 0, 10, 2, 254, 6, 249, 15, 0, 6, 115, 185, 10, 18 | 0, 10, 2, 123, 210, 5, 0, 4, 111, 186, 10, 0, 10, 22, 40, 101, 6, 0, 10, 111, 221, 19 | 0, 0, 43, 40, 188, 10, 0, 10, 125, 209, 5, 0, 4, 42, 2, 123, 212, 5, 0, 4, 111, 20 | 103, 17, 0, 6, 111, 216, 9, 0, 6, 2, 123, 211, 5, 0, 4, 111, 166, 14, 0, 6, 111, 21 | 125, 16, 0, 6, 254, 1, 22, 254, 1, 114, 235, 103, 1, 112, 40, 160, 22, 0, 6, 114, 22 | 160, 104, 1, 112, 40, 210, 0, 0, 10, 114, 194, 5, 0, 112, 40, 221, 0, 0, 10, 44, 51, 23 | 2, 40, 184, 10, 0, 10, 2, 254, 6, 250, 15, 0, 6, 115, 185, 10, 0, 10, 2, 123, 210, 24 | 5, 0, 4, 111, 186, 10, 0, 10, 22, 40, 196, 21, 0, 6, 111, 221, 0, 0, 43, 40, 188, 25 | 10, 0, 10, 125, 209, 5, 0, 4, 42, 2, 40, 184, 10, 0, 10, 2, 254, 6, 251, 15, 0, 6, 26 | 115, 185, 10, 0, 10, 2, 123, 210, 5, 0, 4, 111, 186, 10, 0, 10, 24, 40, 101, 6, 0, 27 | 10, 111, 221, 0, 0, 43, 40, 188, 10, 0, 10, 125, 209, 5, 0, 4, 42 28 | } }, 29 | 30 | // https://github.com/benaadams/Ben.Demystifier/issues/56#issuecomment-390654651 31 | { new byte[] { 32 | 115, 31, 5, 0, 6, 37, 2, 125, 94, 1, 0, 4, 37, 3, 125, 91, 1, 0, 4, 37, 4, 125, 92, 33 | 1, 0, 4, 37, 5, 125, 93, 1, 0, 4, 37, 123, 91, 1, 0, 4, 40, 61, 0, 0, 10, 44, 16, 34 | 40, 160, 4, 0, 6, 114, 253, 15, 0, 112, 115, 90, 0, 0, 10, 122, 254, 6, 32, 5, 0, 35 | 6, 115, 137, 2, 0, 10, 115, 61, 2, 0, 6, 42 36 | } }, 37 | 38 | { new byte[] { 39 | 31, 254, 115, 42, 2, 0, 6, 37, 2, 125, 159, 0, 0, 4, 37, 3, 125, 158, 0, 0, 4, 42 40 | } }, 41 | }; 42 | 43 | // https://github.com/benaadams/Ben.Demystifier/issues/56 44 | [Theory, MemberData(nameof(InlineCilSamples))] 45 | public void ReadsInlinedOpcodes(byte[] cil) 46 | { 47 | var sut = new ILReader(cil); 48 | while (sut.Read(GetType().GetMethod(nameof(ReadsInlinedOpcodes)))) 49 | { 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/InheritenceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Ben.Demystifier.Test 8 | { 9 | public class InheritenceTests 10 | { 11 | private abstract class BaseClass 12 | { 13 | public abstract Task Method(); 14 | } 15 | 16 | private class ImplClass : BaseClass 17 | { 18 | [MethodImpl(MethodImplOptions.NoInlining)] 19 | public override Task Method() 20 | { 21 | throw new Exception(); 22 | } 23 | } 24 | 25 | [Fact] 26 | public async Task ImplementedAbstractMethodDoesNotThrow() 27 | { 28 | // Arrange 29 | var instance = new ImplClass(); 30 | 31 | // Act 32 | Exception exception = null; 33 | try 34 | { 35 | await instance.Method(); 36 | } 37 | catch (Exception ex) 38 | { 39 | exception = ex; 40 | } 41 | 42 | // Act 43 | var est = new EnhancedStackTrace(exception); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/LineEndingsHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace Ben.Demystifier.Test 4 | { 5 | internal static class LineEndingsHelper 6 | { 7 | private static readonly Regex ReplaceLineEndings = new Regex(" in [^\n\r]+"); 8 | 9 | public static string RemoveLineEndings(string original) 10 | { 11 | return ReplaceLineEndings.Replace(original, ""); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/MethodTests.cs: -------------------------------------------------------------------------------- 1 | namespace Ben.Demystifier.Test 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | public class MethodTests 9 | { 10 | [Fact] 11 | public void DemistifiesMethodWithNullableInt() 12 | { 13 | Exception dex = null; 14 | try 15 | { 16 | MethodWithNullableInt(1); 17 | } 18 | catch (Exception e) 19 | { 20 | dex = e.Demystify(); 21 | } 22 | 23 | // Assert 24 | var stackTrace = dex.ToString(); 25 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 26 | var trace = string.Join(string.Empty, stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 27 | 28 | var expected = string.Join(string.Empty, 29 | "System.ArgumentException: Value does not fall within the expected range.", 30 | " at bool Ben.Demystifier.Test.MethodTests.MethodWithNullableInt(int? number)", 31 | " at void Ben.Demystifier.Test.MethodTests.DemistifiesMethodWithNullableInt()"); 32 | 33 | Assert.Equal(expected, trace); 34 | } 35 | 36 | [Fact] 37 | public void DemistifiesMethodWithDynamic() 38 | { 39 | Exception dex = null; 40 | try 41 | { 42 | MethodWithDynamic(1); 43 | } 44 | catch (Exception e) 45 | { 46 | dex = e.Demystify(); 47 | } 48 | 49 | // Assert 50 | var stackTrace = dex.ToString(); 51 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 52 | var trace = string.Join(string.Empty, stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 53 | 54 | var expected = string.Join(string.Empty, 55 | "System.ArgumentException: Value does not fall within the expected range.", 56 | " at bool Ben.Demystifier.Test.MethodTests.MethodWithDynamic(dynamic value)", 57 | " at void Ben.Demystifier.Test.MethodTests.DemistifiesMethodWithDynamic()"); 58 | 59 | Assert.Equal(expected, trace); 60 | } 61 | 62 | [Fact] 63 | public void DemistifiesMethodWithLambda() 64 | { 65 | Exception dex = null; 66 | try 67 | { 68 | MethodWithLambda(); 69 | } 70 | catch (Exception e) 71 | { 72 | dex = e.Demystify(); 73 | } 74 | 75 | // Assert 76 | var stackTrace = dex.ToString(); 77 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 78 | var trace = string.Join(string.Empty, stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 79 | 80 | var expected = string.Join(string.Empty, 81 | "System.ArgumentException: Value does not fall within the expected range.", 82 | " at void Ben.Demystifier.Test.MethodTests.MethodWithLambda()+() => { }", 83 | " at void Ben.Demystifier.Test.MethodTests.MethodWithLambda()", 84 | " at void Ben.Demystifier.Test.MethodTests.DemistifiesMethodWithLambda()"); 85 | 86 | Assert.Equal(expected, trace); 87 | } 88 | 89 | [Fact] 90 | public async Task DemistifiesMethodWithAsyncLambda() 91 | { 92 | Exception dex = null; 93 | try 94 | { 95 | await MethodWithAsyncLambda(); 96 | } 97 | catch (Exception e) 98 | { 99 | dex = e.Demystify(); 100 | } 101 | 102 | // Assert 103 | var stackTrace = dex.ToString(); 104 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 105 | var trace = string.Join(string.Empty, stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 106 | 107 | var expected = string.Join(string.Empty, 108 | "System.ArgumentException: Value does not fall within the expected range.", 109 | " at async Task Ben.Demystifier.Test.MethodTests.MethodWithAsyncLambda()+(?) => { }", 110 | " at async Task Ben.Demystifier.Test.MethodTests.MethodWithAsyncLambda()", 111 | " at async Task Ben.Demystifier.Test.MethodTests.DemistifiesMethodWithAsyncLambda()"); 112 | 113 | Assert.Equal(expected, trace); 114 | } 115 | 116 | private bool MethodWithNullableInt(int? number) => throw new ArgumentException(); 117 | 118 | private bool MethodWithDynamic(dynamic value) => throw new ArgumentException(); 119 | 120 | private void MethodWithLambda() 121 | { 122 | Func action = () => throw new ArgumentException(); 123 | action(); 124 | } 125 | 126 | private async Task MethodWithAsyncLambda() 127 | { 128 | Func action = async () => 129 | { 130 | await Task.CompletedTask; 131 | throw new ArgumentException(); 132 | }; 133 | 134 | await action(); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/MixedStack.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Ben.Demystifier.Test 10 | { 11 | public class MixedStack 12 | { 13 | [Fact] 14 | public void ProducesReadableFrames() 15 | { 16 | // Arrange 17 | var exception = GetMixedStackException(); 18 | 19 | // Act 20 | var methodNames = new EnhancedStackTrace(exception) 21 | .Select( 22 | stackFrame => stackFrame.MethodInfo.ToString() 23 | ) 24 | // Remove Framework method that can be optimized out (inlined) 25 | .Where(methodName => !methodName.StartsWith("bool System.Collections.Generic.List+")); 26 | 27 | var count = methodNames.Count(); 28 | methodNames = methodNames.Take(count - 1); 29 | 30 | // Assert 31 | var expected = ExpectedCallStack.ToArray(); 32 | var trace = methodNames.ToArray(); 33 | 34 | Assert.Equal(expected.Length, trace.Length); 35 | 36 | for (var i = 0; i < expected.Length; i++) 37 | { 38 | Assert.Equal(expected[i], trace[i]); 39 | } 40 | } 41 | 42 | Exception GetMixedStackException() 43 | { 44 | Exception exception = null; 45 | try 46 | { 47 | Start((val: "", true)); 48 | } 49 | catch (Exception ex) 50 | { 51 | exception = ex; 52 | } 53 | 54 | return exception; 55 | } 56 | 57 | static List ExpectedCallStack = new List() 58 | { 59 | "IEnumerable Ben.Demystifier.Test.MixedStack.Iterator()+MoveNext()", 60 | "string string.Join(string separator, IEnumerable values)", 61 | "string Ben.Demystifier.Test.MixedStack+GenericClass.GenericMethod(ref V value)", 62 | "async Task Ben.Demystifier.Test.MixedStack.MethodAsync(int value)", 63 | "async ValueTask Ben.Demystifier.Test.MixedStack.MethodAsync(TValue value)", 64 | "(string val, bool) Ben.Demystifier.Test.MixedStack.Method(string value)", 65 | "ref string Ben.Demystifier.Test.MixedStack.RefMethod(string value)", 66 | "(string val, bool) Ben.Demystifier.Test.MixedStack.s_func(string s, bool b)", 67 | "void Ben.Demystifier.Test.MixedStack.s_action(string s, bool b)", 68 | "void Ben.Demystifier.Test.MixedStack.Start((string val, bool) param)" 69 | 70 | }; 71 | 72 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 73 | static IEnumerable Iterator() 74 | { 75 | var list = new List() { 1, 2, 3, 4 }; 76 | foreach (var item in list) 77 | { 78 | // Throws the exception 79 | list.Add(item); 80 | 81 | yield return item.ToString(); 82 | } 83 | } 84 | 85 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 86 | static async Task MethodAsync(int value) 87 | { 88 | await Task.Delay(0); 89 | return GenericClass.GenericMethod(ref value); 90 | } 91 | 92 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 93 | static async ValueTask MethodAsync(TValue value) => await MethodAsync(1); 94 | 95 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 96 | static (string val, bool) Method(string value) => (MethodAsync(value).GetAwaiter().GetResult(), true); 97 | 98 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 99 | static ref string RefMethod(string value) 100 | { 101 | Method(value); 102 | return ref s; 103 | } 104 | 105 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 106 | static void Start((string val, bool) param) => s_action.Invoke(param.val, param.Item2); 107 | 108 | static Action s_action = (string s, bool b) => s_func(s, b); 109 | static Func s_func = (string s, bool b) => (RefMethod(s), b); 110 | static string s = ""; 111 | 112 | static class GenericClass 113 | { 114 | [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 115 | public static string GenericMethod(ref V value) 116 | { 117 | var returnVal = ""; 118 | for (var i = 0; i < 10; i++) 119 | { 120 | returnVal += string.Join(", ", Iterator()); 121 | } 122 | return returnVal; 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/NonThrownException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Ben.Demystifier.Test 9 | { 10 | public class NonThrownException 11 | { 12 | [Fact] 13 | public async Task DoesNotPreventThrowStackTrace() 14 | { 15 | // Arrange 16 | Exception innerException = null; 17 | try 18 | { 19 | await Task.Run(() => throw new Exception()).ConfigureAwait(false); 20 | } 21 | catch(Exception ex) 22 | { 23 | innerException = ex; 24 | } 25 | 26 | // Act 27 | var demystifiedException = new Exception(innerException.Message, innerException).Demystify(); 28 | 29 | // Assert 30 | var stackTrace = demystifiedException.ToString(); 31 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 32 | var trace = stackTrace.Split(new[]{Environment.NewLine}, StringSplitOptions.None); 33 | 34 | #if NETCOREAPP3_0_OR_GREATER 35 | Assert.Equal( 36 | new[] { 37 | "System.Exception: Exception of type 'System.Exception' was thrown.", 38 | " ---> System.Exception: Exception of type 'System.Exception' was thrown.", 39 | " at Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }", 40 | " at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()", 41 | " --- End of inner exception stack trace ---"}, 42 | trace); 43 | #else 44 | Assert.Equal( 45 | new[] { 46 | "System.Exception: Exception of type 'System.Exception' was thrown. ---> System.Exception: Exception of type 'System.Exception' was thrown.", 47 | " at Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }", 48 | " at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()", 49 | " --- End of inner exception stack trace ---"}, 50 | trace); 51 | #endif 52 | 53 | // Act 54 | try 55 | { 56 | throw demystifiedException; 57 | } 58 | catch (Exception ex) 59 | { 60 | demystifiedException = ex.Demystify(); 61 | } 62 | 63 | // Assert 64 | stackTrace = demystifiedException.ToString(); 65 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 66 | trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None); 67 | 68 | #if NETCOREAPP3_0_OR_GREATER 69 | Assert.Equal( 70 | new[] { 71 | "System.Exception: Exception of type 'System.Exception' was thrown.", 72 | " ---> System.Exception: Exception of type 'System.Exception' was thrown.", 73 | " at Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }", 74 | " at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()", 75 | " --- End of inner exception stack trace ---", 76 | " at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()" 77 | }, 78 | trace); 79 | #else 80 | Assert.Equal( 81 | new[] { 82 | "System.Exception: Exception of type 'System.Exception' was thrown. ---> System.Exception: Exception of type 'System.Exception' was thrown.", 83 | " at Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()+() => { }", 84 | " at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()", 85 | " --- End of inner exception stack trace ---", 86 | " at async Task Ben.Demystifier.Test.NonThrownException.DoesNotPreventThrowStackTrace()" 87 | }, 88 | trace); 89 | #endif 90 | } 91 | 92 | [Fact] 93 | public async Task Current() 94 | { 95 | // Arrange 96 | EnhancedStackTrace est = null; 97 | 98 | // Act 99 | await Task.Run(() => est = EnhancedStackTrace.Current()).ConfigureAwait(false); 100 | 101 | // Assert 102 | var stackTrace = est.ToString(); 103 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 104 | var trace = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None) 105 | // Remove Full framework entries 106 | .Where(s => !s.StartsWith(" at bool System.Threading._ThreadPoolWaitCallbac") && 107 | !s.StartsWith(" at void System.Threading.Tasks.Task.System.Thre")); 108 | 109 | 110 | Assert.Equal( 111 | new[] { 112 | " at bool System.Threading.ThreadPoolWorkQueue.Dispatch()", 113 | #if NET6_0_OR_GREATER 114 | " at void System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()", 115 | " at void System.Threading.Thread.StartCallback()", 116 | #endif 117 | }, 118 | trace); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/ParameterParamTests.cs: -------------------------------------------------------------------------------- 1 | namespace Ben.Demystifier.Test 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using Xunit; 6 | 7 | public class ParameterParamTests 8 | { 9 | [Fact] 10 | public void DemistifiesMethodWithParams() 11 | { 12 | Exception dex = null; 13 | try 14 | { 15 | MethodWithParams(1, 2, 3); 16 | } 17 | catch (Exception e) 18 | { 19 | dex = e.Demystify(); 20 | } 21 | 22 | // Assert 23 | var stackTrace = dex.ToString(); 24 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 25 | var trace = string.Join(string.Empty, stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 26 | 27 | var expected = string.Join(string.Empty, new[] { 28 | "System.ArgumentException: Value does not fall within the expected range.", 29 | " at bool Ben.Demystifier.Test.ParameterParamTests.MethodWithParams(params int[] numbers)", 30 | " at void Ben.Demystifier.Test.ParameterParamTests.DemistifiesMethodWithParams()"}); 31 | 32 | Assert.Equal(expected, trace); 33 | } 34 | 35 | private bool MethodWithParams(params int[] numbers) 36 | { 37 | throw new ArgumentException(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/RecursionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Ben.Demystifier.Test 9 | { 10 | public class RecursionTests 11 | { 12 | [Fact] 13 | public async Task DemystifiesAsyncRecursion() 14 | { 15 | Exception demystifiedException = null; 16 | 17 | try 18 | { 19 | await RecurseAsync(10); 20 | } 21 | catch (Exception ex) 22 | { 23 | demystifiedException = ex.Demystify(); 24 | } 25 | 26 | // Assert 27 | var stackTrace = demystifiedException.ToString(); 28 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 29 | var traces = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) 30 | .Select(s => Regex.Replace(s, " x [0-9]+", " x N")) 31 | .Skip(1) 32 | .ToArray(); 33 | 34 | Assert.Contains(" at async Task Ben.Demystifier.Test.RecursionTests.RecurseAsync(int depth) x N", traces); 35 | } 36 | 37 | [Fact] 38 | public void DemystifiesRecursion() 39 | { 40 | Exception demystifiedException = null; 41 | 42 | try 43 | { 44 | Recurse(10); 45 | } 46 | catch (Exception ex) 47 | { 48 | demystifiedException = ex.Demystify(); 49 | } 50 | 51 | // Assert 52 | var stackTrace = demystifiedException.ToString(); 53 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 54 | var traces = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) 55 | .Select(s => Regex.Replace(s, " x [0-9]+", " x N")) 56 | .Skip(1) 57 | .ToArray(); 58 | 59 | Assert.Contains(" at int Ben.Demystifier.Test.RecursionTests.Recurse(int depth) x N", traces); 60 | } 61 | 62 | async Task RecurseAsync(int depth) 63 | { 64 | if (depth > 0) await RecurseAsync(depth - 1); 65 | throw new InvalidOperationException(); 66 | } 67 | 68 | int Recurse(int depth) 69 | { 70 | if (depth > 0) Recurse(depth - 1); 71 | throw new InvalidOperationException(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/ReflectionHelperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Internal; 3 | using Xunit; 4 | 5 | namespace Ben.Demystifier.Test 6 | { 7 | public class ReflectionHelperTest 8 | { 9 | [Fact] 10 | public void IsValueTupleReturnsTrueForTupleWith1Element() 11 | { 12 | Assert.True(typeof(ValueTuple).IsValueTuple()); 13 | } 14 | 15 | [Fact] 16 | public void IsValueTupleReturnsTrueForTupleWith1ElementWithOpenedType() 17 | { 18 | Assert.True(typeof(ValueTuple<>).IsValueTuple()); 19 | } 20 | 21 | [Fact] 22 | public void IsValueTupleReturnsTrueForTupleWith6ElementsWithOpenedType() 23 | { 24 | Assert.True(typeof(ValueTuple<,,,,,>).IsValueTuple()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/ResolvedMethodTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq; 3 | using System.Text; 4 | using Xunit; 5 | 6 | namespace Ben.Demystifier.Test 7 | { 8 | public class ResolvedMethodTests 9 | { 10 | [Fact] 11 | public void AppendWithFullNameTrueTest() 12 | { 13 | var resolvedMethod = EnhancedStackTrace.GetMethodDisplayString(GetType().GetMethods().First(m => m.Name == nameof(AppendWithFullNameTrueTest))); 14 | var sb = new StringBuilder(); 15 | Assert.Equal($"void {GetType().Namespace}.{GetType().Name}.{nameof(AppendWithFullNameTrueTest)}()", resolvedMethod.Append(sb).ToString()); 16 | } 17 | 18 | [Fact] 19 | public void AppendWithFullNameFalseTest() 20 | { 21 | var resolvedMethod = EnhancedStackTrace.GetMethodDisplayString(GetType().GetMethods().First(m => m.Name == nameof(AppendWithFullNameFalseTest))); 22 | var sb = new StringBuilder(); 23 | Assert.Equal($"void {GetType().Name}.{nameof(AppendWithFullNameFalseTest)}()", resolvedMethod.Append(sb, false).ToString()); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/ToDemystifiedStringTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace Ben.Demystifier.Test 8 | { 9 | public sealed class ToDemystifiedStringTests 10 | { 11 | private readonly ITestOutputHelper _output; 12 | 13 | public ToDemystifiedStringTests(ITestOutputHelper output) 14 | { 15 | _output = output; 16 | } 17 | 18 | [Fact] 19 | public void DemystifyShouldNotAffectTheOriginalStackTrace() 20 | { 21 | try 22 | { 23 | SimpleMethodThatThrows(null).Wait(); 24 | } 25 | catch (Exception e) 26 | { 27 | var original = e.ToString(); 28 | var stringDemystified = e.ToStringDemystified(); 29 | 30 | _output.WriteLine("Demystified: "); 31 | _output.WriteLine(stringDemystified); 32 | 33 | _output.WriteLine("Original: "); 34 | var afterDemystified = e.ToString(); 35 | _output.WriteLine(afterDemystified); 36 | 37 | Assert.Equal(original, afterDemystified); 38 | } 39 | 40 | async Task SimpleMethodThatThrows(string value) 41 | { 42 | if (value == null) 43 | { 44 | throw new InvalidOperationException("message"); 45 | } 46 | 47 | await Task.Yield(); 48 | } 49 | } 50 | 51 | 52 | [Fact] 53 | public void DemystifyKeepsMessage() 54 | { 55 | Exception ex = null; 56 | try 57 | { 58 | throw new InvalidOperationException("aaa") 59 | { 60 | Data = 61 | { 62 | ["bbb"] = "ccc", 63 | ["ddd"] = "eee", 64 | } 65 | }; 66 | } 67 | catch (Exception e) 68 | { 69 | ex = e; 70 | } 71 | 72 | var original = ex.ToString(); 73 | var endLine = (int)Math.Min((uint)original.IndexOf('\n'), original.Length); 74 | 75 | original = original.Substring(0, endLine); 76 | 77 | var stringDemystified = ex.ToStringDemystified(); 78 | endLine = (int)Math.Min((uint)stringDemystified.IndexOf('\n'), stringDemystified.Length); 79 | 80 | stringDemystified = stringDemystified.Substring(0, endLine); 81 | 82 | Assert.Equal(original, stringDemystified); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/TuplesTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Ben.Demystifier.Test 8 | { 9 | public class TuplesTests 10 | { 11 | [Fact] 12 | public void DemistifiesAsyncMethodWithTuples() 13 | { 14 | Exception demystifiedException = null; 15 | 16 | try 17 | { 18 | AsyncThatReturnsTuple().GetAwaiter().GetResult(); 19 | } 20 | catch (Exception ex) 21 | { 22 | demystifiedException = ex.Demystify(); 23 | } 24 | 25 | // Assert 26 | var stackTrace = demystifiedException.ToString(); 27 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 28 | var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 29 | 30 | var expected = string.Join("", new[] { 31 | "System.ArgumentException: Value does not fall within the expected range.", 32 | " at async Task<(int left, int right)> Ben.Demystifier.Test.TuplesTests.AsyncThatReturnsTuple()", 33 | " at void Ben.Demystifier.Test.TuplesTests.DemistifiesAsyncMethodWithTuples()"}); 34 | 35 | Assert.Equal(expected, trace); 36 | } 37 | 38 | [Fact] 39 | public void DemistifiesListOfTuples() 40 | { 41 | Exception demystifiedException = null; 42 | 43 | try 44 | { 45 | ListOfTuples(); 46 | } 47 | catch (Exception ex) 48 | { 49 | demystifiedException = ex.Demystify(); 50 | } 51 | 52 | // Assert 53 | var stackTrace = demystifiedException.ToString(); 54 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 55 | var trace = string.Join("", stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 56 | 57 | var expected = string.Join("", new[] { 58 | "System.ArgumentException: Value does not fall within the expected range.", 59 | " at List<(int left, int right)> Ben.Demystifier.Test.TuplesTests.ListOfTuples()", 60 | " at void Ben.Demystifier.Test.TuplesTests.DemistifiesListOfTuples()"}); 61 | 62 | Assert.Equal(expected, trace); 63 | } 64 | 65 | async Task<(int left, int right)> AsyncThatReturnsTuple() 66 | { 67 | await Task.Delay(1).ConfigureAwait(false); 68 | throw new ArgumentException(); 69 | } 70 | 71 | List<(int left, int right)> ListOfTuples() 72 | { 73 | throw new ArgumentException(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/TypeNameTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Xunit; 4 | 5 | namespace Ben.Demystifier.Test 6 | { 7 | public class TypeNameTests 8 | { 9 | [Fact] 10 | public void NestedGenericTypes() 11 | { 12 | try 13 | { 14 | Throw(new Generic<(int, string)>.Nested()); 15 | } 16 | catch (Exception ex) 17 | { 18 | var text = ex.ToStringDemystified(); 19 | } 20 | } 21 | 22 | private void Throw(Generic<(int a, string b)>.Nested nested) 23 | { 24 | throw null; 25 | } 26 | } 27 | 28 | public static class Generic { public struct Nested { } } 29 | } 30 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.1", 3 | "publicReleaseRefSpec": [ 4 | "^refs/heads/main", // we release out of master 5 | "^refs/heads/dev$", // we release out of develop 6 | "^refs/tags/v\\d+\\.\\d+" // we also release tags starting with vN.N 7 | ], 8 | "nugetPackageVersion":{ 9 | "semVer": 2 10 | }, 11 | "cloudBuild": { 12 | "buildNumber": { 13 | "enabled": true, 14 | "setVersionVariables": true 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------