├── .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 | [](https://www.nuget.org/packages/Ben.Demystifier/)
3 | [](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 | [](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 |
--------------------------------------------------------------------------------