├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── pull-request.yaml ├── images └── icon.png ├── src └── Ben.Demystifier │ ├── key.snk │ ├── Enumerable │ ├── IEnumerableIList.cs │ ├── EnumeratorIList.cs │ └── EnumerableIList.cs │ ├── Constants.cs │ ├── StringBuilderExtentions.cs │ ├── ResolvedParameter.cs │ ├── ValueTupleResolvedParameter.cs │ ├── Ben.Demystifier.csproj │ ├── Internal │ ├── ReflectionHelper.cs │ ├── ILReader.cs │ └── PortablePdbReader.cs │ ├── ExceptionExtensions.cs │ ├── EnhancedStackFrame.cs │ ├── EnhancedStackTrace.cs │ ├── ResolvedMethod.cs │ ├── TypeNameHelper.cs │ └── EnhancedStackTrace.Frames.cs ├── test ├── Ben.Demystifier.Test │ ├── LineEndingsHelper.cs │ ├── TypeNameTests.cs │ ├── ReflectionHelperTests.cs │ ├── Ben.Demystifier.Test.csproj │ ├── ResolvedMethodTests.cs │ ├── InheritenceTests.cs │ ├── GenericMethodDisplayStringTests.cs │ ├── ParameterParamTests.cs │ ├── EnhancedStackTraceTests.cs │ ├── DynamicCompilation.cs │ ├── RecursionTests.cs │ ├── AsyncEnumerableTests.cs │ ├── ToDemystifiedStringTests.cs │ ├── TuplesTests.cs │ ├── ILReaderTests.cs │ ├── AggregateException.cs │ ├── MixedStack.cs │ ├── NonThrownException.cs │ └── MethodTests.cs └── Ben.Demystifier.Benchmarks │ ├── Ben.Demystifier.Benchmarks.csproj │ ├── Exceptions.cs │ └── Program.cs ├── version.json ├── sample ├── StackTrace │ ├── StackTrace.csproj │ └── Program.cs └── FSharpStackTrace │ ├── FSharpStackTrace.fsproj │ └── Program.fs ├── Directory.Build.props ├── .editorconfig ├── Ben.Demystifier.sln ├── .gitignore ├── README.md └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | github: [benaadams] 3 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/Ben.Demystifier/HEAD/images/icon.png -------------------------------------------------------------------------------- /src/Ben.Demystifier/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/Ben.Demystifier/HEAD/src/Ben.Demystifier/key.snk -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Ben.Demystifier; 2 | 3 | internal static class Constants 4 | { 5 | internal const string TrimWarning = "This class should be avoided when compiling for AOT."; 6 | internal const string SuppressionResurfaced = "Surfaced by parent class"; 7 | internal const string AvoidAtRuntime = "Non-trimmable code is avoided at runtime"; 8 | internal const string SingleFileFallback = "Fallback functionality for single files"; 9 | } 10 | -------------------------------------------------------------------------------- /sample/StackTrace/StackTrace.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Benchmarks/Ben.Demystifier.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net462;net6.0 4 | Release 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/FSharpStackTrace/FSharpStackTrace.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | $(NoWarn);NU1902;NU1903 6 | 7 | NU1901;NU1902;NU1903;NU1904 8 | 9 | 10 | 10 11 | 12 | 13 | 14 | 15 | 16 | true 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/Ben.Demystifier.Test/Ben.Demystifier.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | ..\..\src\Ben.Demystifier\key.snk 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [windows-latest, ubuntu-latest, macOS-latest] 16 | config: [Debug, Release] 17 | steps: 18 | - name: Clone source 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set environment variables 24 | run: | 25 | echo "DOTNET_NOLOGO=true" >> $GITHUB_ENV 26 | 27 | - name: Install .NET SDK 28 | uses: actions/setup-dotnet@v4 29 | with: 30 | dotnet-version: | 31 | 6.0.417 32 | 9.0.100 33 | 34 | - name: Get .NET information 35 | run: dotnet --info 36 | 37 | - name: Build 38 | run: dotnet build -c ${{ matrix.config }} 39 | 40 | - name: "Test" 41 | run: dotnet test -c ${{ matrix.config }} -l "trx;LogFilePrefix=testresults_${{ runner.os }}_${{ matrix.config }}" 42 | 43 | - name: "Upload Test Results" 44 | if: always() 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: test-results-${{ runner.os }}-${{ matrix.config }}.trx 48 | path: '**/testresults_*.trx' 49 | -------------------------------------------------------------------------------- /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.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.HostProcess)] 10 | [SimpleJob(RuntimeMoniker.Net462)] 11 | [Config(typeof(Config))] 12 | public class ExceptionTests 13 | { 14 | [Benchmark(Baseline = true, Description = ".ToString()")] 15 | public string Baseline() => new Exception().ToString(); 16 | 17 | [Benchmark(Description = "Demystify().ToString()")] 18 | public string Demystify() => new Exception().Demystify().ToString(); 19 | 20 | [Benchmark(Description = "(left, right).ToString()")] 21 | public string ToStringForTupleBased() => GetException(() => ReturnsTuple()).ToString(); 22 | 23 | [Benchmark(Description = "(left, right).Demystify().ToString()")] 24 | public string ToDemystifyForTupleBased() => GetException(() => ReturnsTuple()).Demystify().ToString(); 25 | 26 | private static Exception GetException(Action action) 27 | { 28 | try 29 | { 30 | action(); 31 | throw new InvalidOperationException("Should not be reachable."); 32 | } 33 | catch (Exception e) 34 | { 35 | return e; 36 | } 37 | } 38 | 39 | private static List<(int left, int right)> ReturnsTuple() => throw new Exception(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 6 | using System.Text; 7 | using Ben.Demystifier; 8 | 9 | namespace System.Diagnostics 10 | { 11 | #if NET6_0_OR_GREATER 12 | [RequiresUnreferencedCode(Constants.TrimWarning)] 13 | #endif 14 | internal static class StringBuilderExtentions 15 | { 16 | public static StringBuilder AppendDemystified(this StringBuilder builder, Exception exception) 17 | { 18 | try 19 | { 20 | var stackTrace = new EnhancedStackTrace(exception); 21 | 22 | builder.Append(exception.GetType()); 23 | if (!string.IsNullOrEmpty(exception.Message)) 24 | { 25 | builder.Append(": ").Append(exception.Message); 26 | } 27 | builder.Append(Environment.NewLine); 28 | 29 | if (stackTrace.FrameCount > 0) 30 | { 31 | stackTrace.Append(builder); 32 | } 33 | 34 | if (exception is AggregateException aggEx) 35 | { 36 | foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions)) 37 | { 38 | builder.AppendInnerException(ex); 39 | } 40 | } 41 | 42 | if (exception.InnerException != null) 43 | { 44 | builder.AppendInnerException(exception.InnerException); 45 | } 46 | } 47 | catch 48 | { 49 | // Processing exceptions shouldn't throw exceptions; if it fails 50 | } 51 | 52 | return builder; 53 | } 54 | 55 | private static void AppendInnerException(this StringBuilder builder, Exception exception) 56 | => builder.Append(" ---> ") 57 | .AppendDemystified(exception) 58 | .AppendLine() 59 | .Append(" --- End of inner exception stack trace ---"); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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; 5 | using System.Diagnostics; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Text; 8 | using Ben.Demystifier; 9 | 10 | namespace System.Diagnostics 11 | 12 | { 13 | #if NET6_0_OR_GREATER 14 | [RequiresUnreferencedCode(Constants.TrimWarning)] 15 | #endif 16 | public class ResolvedParameter 17 | { 18 | public string? Name { get; set; } 19 | 20 | public Type ResolvedType { get; set; } 21 | 22 | public string? Prefix { get; set; } 23 | public bool IsDynamicType { get; set; } 24 | 25 | public ResolvedParameter(Type resolvedType) => ResolvedType = resolvedType; 26 | 27 | public override string ToString() => Append(new StringBuilder()).ToString(); 28 | 29 | #if NET6_0_OR_GREATER 30 | [UnconditionalSuppressMessage("SingleFile", "IL3002: calling members marked with 'RequiresAssemblyFilesAttribute'", Justification = Constants.SingleFileFallback)] 31 | #endif 32 | public StringBuilder Append(StringBuilder sb) 33 | { 34 | if (ResolvedType.Assembly.ManifestModule.Name == "FSharp.Core.dll" && ResolvedType.Name == "Unit") 35 | return sb; 36 | 37 | if (!string.IsNullOrEmpty(Prefix)) 38 | { 39 | sb.Append(Prefix) 40 | .Append(" "); 41 | } 42 | 43 | if (IsDynamicType) 44 | { 45 | sb.Append("dynamic"); 46 | } 47 | else if (ResolvedType != null) 48 | { 49 | AppendTypeName(sb); 50 | } 51 | else 52 | { 53 | sb.Append("?"); 54 | } 55 | 56 | if (!string.IsNullOrEmpty(Name)) 57 | { 58 | sb.Append(" ") 59 | .Append(Name); 60 | } 61 | 62 | return sb; 63 | } 64 | 65 | protected virtual void AppendTypeName(StringBuilder sb) 66 | { 67 | sb.AppendTypeDisplayName(ResolvedType, fullName: false, includeGenericParameterNames: true); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | internal 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.CodeAnalysis; 6 | using System.Diagnostics.Internal; 7 | using System.Text; 8 | using Ben.Demystifier; 9 | 10 | namespace System.Diagnostics 11 | { 12 | #if NET6_0_OR_GREATER 13 | [RequiresUnreferencedCode(Constants.TrimWarning)] 14 | #endif 15 | internal class ValueTupleResolvedParameter : ResolvedParameter 16 | { 17 | public IList TupleNames { get; } 18 | 19 | public ValueTupleResolvedParameter(Type resolvedType, IList tupleNames) 20 | : base(resolvedType) 21 | => TupleNames = tupleNames; 22 | 23 | protected override void AppendTypeName(StringBuilder sb) 24 | { 25 | if (ResolvedType is not null) 26 | { 27 | if (ResolvedType.IsValueTuple()) 28 | { 29 | AppendValueTupleParameterName(sb, ResolvedType); 30 | } 31 | else 32 | { 33 | // Need to unwrap the first generic argument first. 34 | sb.Append(TypeNameHelper.GetTypeNameForGenericType(ResolvedType)); 35 | sb.Append("<"); 36 | AppendValueTupleParameterName(sb, ResolvedType.GetGenericArguments()[0]); 37 | sb.Append(">"); 38 | } 39 | } 40 | } 41 | 42 | private void AppendValueTupleParameterName(StringBuilder sb, Type parameterType) 43 | { 44 | sb.Append("("); 45 | var args = parameterType.GetGenericArguments(); 46 | for (var i = 0; i < args.Length; i++) 47 | { 48 | if (i > 0) 49 | { 50 | sb.Append(", "); 51 | } 52 | 53 | sb.AppendTypeDisplayName(args[i], fullName: false, includeGenericParameterNames: true); 54 | 55 | if (i >= TupleNames.Count) 56 | { 57 | continue; 58 | } 59 | 60 | var argName = TupleNames[i]; 61 | if (argName == null) 62 | { 63 | continue; 64 | } 65 | 66 | sb.Append(" "); 67 | sb.Append(argName); 68 | } 69 | 70 | sb.Append(")"); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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.0;netstandard2.1;net462;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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 6 | using System.Reflection; 7 | using System.Threading; 8 | using Ben.Demystifier; 9 | 10 | namespace System.Diagnostics.Internal 11 | { 12 | /// 13 | /// A helper class that contains utilities methods for dealing with reflection. 14 | /// 15 | #if NET6_0_OR_GREATER 16 | [RequiresUnreferencedCode(Constants.TrimWarning)] 17 | #endif 18 | internal static class ReflectionHelper 19 | { 20 | #if NETFRAMEWORK 21 | private static PropertyInfo? transformerNamesLazyPropertyInfo; 22 | #endif 23 | 24 | /// 25 | /// Returns true if the is a value tuple type. 26 | /// 27 | public static bool IsValueTuple(this Type type) 28 | { 29 | return type.Namespace == "System" && type.Name.Contains("ValueTuple`"); 30 | } 31 | 32 | #if NETFRAMEWORK 33 | /// 34 | /// Returns true if the given is of type TupleElementNameAttribute. 35 | /// 36 | /// 37 | /// To avoid compile-time dependency hell with System.ValueTuple, this method uses reflection and not checks statically that 38 | /// the given is TupleElementNameAttribute. 39 | /// 40 | public static bool IsTupleElementNameAttribute(this Attribute attribute) 41 | { 42 | var attributeType = attribute.GetType(); 43 | return attributeType.Namespace == "System.Runtime.CompilerServices" && 44 | attributeType.Name == "TupleElementNamesAttribute"; 45 | } 46 | 47 | /// 48 | /// Returns 'TransformNames' property value from a given . 49 | /// 50 | /// 51 | /// To avoid compile-time dependency hell with System.ValueTuple, this method uses reflection 52 | /// instead of casting the attribute to a specific type. 53 | /// 54 | public static IList? GetTransformerNames(this Attribute attribute) 55 | { 56 | Debug.Assert(attribute.IsTupleElementNameAttribute()); 57 | 58 | var propertyInfo = GetTransformNamesPropertyInfo(attribute.GetType()); 59 | return propertyInfo?.GetValue(attribute) as IList; 60 | } 61 | 62 | private static PropertyInfo? GetTransformNamesPropertyInfo(Type attributeType) 63 | { 64 | #pragma warning disable 8634 65 | return LazyInitializer.EnsureInitialized(ref transformerNamesLazyPropertyInfo, 66 | #pragma warning restore 8634 67 | () => attributeType.GetProperty("TransformNames", BindingFlags.Instance | BindingFlags.Public)!); 68 | } 69 | #endif 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 7 | using System.Reflection; 8 | using System.Text; 9 | using Constants = Ben.Demystifier.Constants; 10 | 11 | namespace System.Diagnostics 12 | { 13 | /// 14 | /// Extension methods to Demystify exveption stack traces 15 | /// 16 | public static class ExceptionExtensions 17 | { 18 | private static readonly FieldInfo? stackTraceString = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic); 19 | 20 | private static void SetStackTracesString(this Exception exception, string value) 21 | => stackTraceString?.SetValue(exception, value); 22 | 23 | /// 24 | /// Demystifies the given and tracks the original stack traces for the whole exception tree. 25 | /// 26 | #if NET6_0_OR_GREATER 27 | [RequiresUnreferencedCode(Constants.TrimWarning)] 28 | #endif 29 | public static T Demystify(this T exception) where T : Exception 30 | { 31 | try 32 | { 33 | var stackTrace = new EnhancedStackTrace(exception); 34 | 35 | if (stackTrace.FrameCount > 0) 36 | { 37 | exception.SetStackTracesString(stackTrace.ToString()); 38 | } 39 | 40 | if (exception is AggregateException aggEx) 41 | { 42 | foreach (var ex in EnumerableIList.Create(aggEx.InnerExceptions)) 43 | { 44 | ex.Demystify(); 45 | } 46 | } 47 | 48 | exception.InnerException?.Demystify(); 49 | } 50 | catch 51 | { 52 | // Processing exceptions shouldn't throw exceptions; if it fails 53 | } 54 | 55 | return exception; 56 | } 57 | 58 | /// 59 | /// Gets demystified string representation of the . 60 | /// 61 | /// 62 | /// method mutates the exception instance that can cause 63 | /// issues if a system relies on the stack trace be in the specific form. 64 | /// Unlike this method is pure. It calls first, 65 | /// computes a demystified string representation and then restores the original state of the exception back. 66 | /// 67 | [Contracts.Pure] 68 | #if NET6_0_OR_GREATER 69 | [RequiresUnreferencedCode(Constants.TrimWarning)] 70 | #endif 71 | public static string ToStringDemystified(this Exception exception) 72 | => new StringBuilder().AppendDemystified(exception).ToString(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | build.ps1 = build.ps1 21 | Directory.Build.props = Directory.Build.props 22 | README.md = README.md 23 | version.json = version.json 24 | EndProjectSection 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ben.Demystifier.Benchmarks", "test\Ben.Demystifier.Benchmarks\Ben.Demystifier.Benchmarks.csproj", "{EF5557DF-C48E-4999-846C-D99A92E86373}" 27 | EndProject 28 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharpStackTrace", "sample\FSharpStackTrace\FSharpStackTrace.fsproj", "{D6B779D2-A678-47CC-A2F9-A312292EA7A2}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Release|Any CPU = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {5410A056-89AB-4912-BD1E-A63616AD91D0}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {E161FC12-53C2-47CD-A5FC-3684B86723A9}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Debug|Any CPU.ActiveCfg = Release|Any CPU 49 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Debug|Any CPU.Build.0 = Release|Any CPU 50 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {EF5557DF-C48E-4999-846C-D99A92E86373}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(NestedProjects) = preSolution 61 | {5410A056-89AB-4912-BD1E-A63616AD91D0} = {A2FCCAAC-BE90-4F7E-B95F-A72D46DDD6B3} 62 | {B9E150B0-AEEB-4D98-8BE1-92C1296699A2} = {59CA6310-4AA5-4093-95D4-472B94DC0CD4} 63 | {E161FC12-53C2-47CD-A5FC-3684B86723A9} = {455921D3-DD54-4355-85CF-F4009DF2AB70} 64 | {EF5557DF-C48E-4999-846C-D99A92E86373} = {59CA6310-4AA5-4093-95D4-472B94DC0CD4} 65 | {D6B779D2-A678-47CC-A2F9-A312292EA7A2} = {455921D3-DD54-4355-85CF-F4009DF2AB70} 66 | EndGlobalSection 67 | GlobalSection(ExtensibilityGlobals) = postSolution 68 | SolutionGuid = {841B7D5F-E810-4F94-A529-002C7E075216} 69 | EndGlobalSection 70 | EndGlobal 71 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 5 | using System.Reflection; 6 | using Ben.Demystifier; 7 | 8 | namespace System.Diagnostics 9 | { 10 | #if NET6_0_OR_GREATER 11 | [RequiresUnreferencedCode(Constants.TrimWarning)] 12 | #endif 13 | public class EnhancedStackFrame : StackFrame 14 | { 15 | private readonly string? _fileName; 16 | private readonly int _lineNumber; 17 | private readonly int _colNumber; 18 | 19 | public StackFrame StackFrame { get; } 20 | 21 | public bool IsRecursive 22 | { 23 | get => MethodInfo.RecurseCount > 0; 24 | internal set => MethodInfo.RecurseCount++; 25 | } 26 | 27 | public ResolvedMethod MethodInfo { get; } 28 | 29 | internal EnhancedStackFrame(StackFrame stackFrame, ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber) 30 | : base(fileName, lineNumber, colNumber) 31 | { 32 | StackFrame = stackFrame; 33 | MethodInfo = methodInfo; 34 | 35 | _fileName = fileName; 36 | _lineNumber = lineNumber; 37 | _colNumber = colNumber; 38 | } 39 | 40 | internal bool IsEquivalent(ResolvedMethod methodInfo, string? fileName, int lineNumber, int colNumber) 41 | { 42 | return _lineNumber == lineNumber && 43 | _colNumber == colNumber && 44 | _fileName == fileName && 45 | MethodInfo.IsSequentialEquivalent(methodInfo); 46 | } 47 | 48 | /// 49 | /// Gets the column number in the file that contains the code that is executing. 50 | /// This information is typically extracted from the debugging symbols for the executable. 51 | /// 52 | /// The file column number, or 0 (zero) if the file column number cannot be determined. 53 | public override int GetFileColumnNumber() => _colNumber; 54 | 55 | /// 56 | /// Gets the line number in the file that contains the code that is executing. 57 | /// This information is typically extracted from the debugging symbols for the executable. 58 | /// 59 | /// The file line number, or 0 (zero) if the file line number cannot be determined. 60 | public override int GetFileLineNumber() => _lineNumber; 61 | 62 | /// 63 | /// Gets the file name that contains the code that is executing. 64 | /// This information is typically extracted from the debugging symbols for the executable. 65 | /// 66 | /// The file name, or null if the file name cannot be determined. 67 | public override string? GetFileName() => _fileName; 68 | 69 | /// 70 | /// Gets the offset from the start of the Microsoft intermediate language (MSIL) 71 | /// code for the method that is executing. This offset might be an approximation 72 | /// depending on whether or not the just-in-time (JIT) compiler is generating debugging 73 | /// code. The generation of this debugging information is controlled by the System.Diagnostics.DebuggableAttribute. 74 | /// 75 | /// The offset from the start of the MSIL code for the method that is executing. 76 | public override int GetILOffset() => StackFrame.GetILOffset(); 77 | 78 | /// 79 | /// Gets the method in which the frame is executing. 80 | /// 81 | /// The method in which the frame is executing. 82 | public override MethodBase? GetMethod() => StackFrame.GetMethod(); 83 | 84 | /// 85 | /// Gets the offset from the start of the native just-in-time (JIT)-compiled code 86 | /// for the method that is being executed. The generation of this debugging information 87 | /// is controlled by the System.Diagnostics.DebuggableAttribute class. 88 | /// 89 | /// The offset from the start of the JIT-compiled code for the method that is being executed. 90 | public override int GetNativeOffset() => StackFrame.GetNativeOffset(); 91 | 92 | /// 93 | /// Builds a readable representation of the stack trace. 94 | /// 95 | /// A readable representation of the stack trace. 96 | public override string ToString() => MethodInfo.ToString(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Ben.Demystifier/Internal/ILReader.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Reflection; 3 | using System.Reflection.Emit; 4 | using Ben.Demystifier; 5 | 6 | namespace System.Diagnostics.Internal 7 | { 8 | internal class ILReader 9 | { 10 | private static OpCode[] singleByteOpCode; 11 | private static OpCode[] doubleByteOpCode; 12 | 13 | private readonly byte[] _cil; 14 | private int ptr; 15 | 16 | public ILReader(byte[] cil) => _cil = cil; 17 | 18 | public OpCode OpCode { get; private set; } 19 | public int MetadataToken { get; private set; } 20 | public MemberInfo? Operand { get; private set; } 21 | 22 | #if NET6_0_OR_GREATER 23 | [RequiresUnreferencedCode(Constants.TrimWarning)] 24 | #endif 25 | public bool Read(MethodBase methodInfo) 26 | { 27 | if (ptr < _cil.Length) 28 | { 29 | OpCode = ReadOpCode(); 30 | Operand = ReadOperand(OpCode, methodInfo); 31 | return true; 32 | } 33 | return false; 34 | } 35 | 36 | OpCode ReadOpCode() 37 | { 38 | var instruction = ReadByte(); 39 | if (instruction < 254) 40 | return singleByteOpCode[instruction]; 41 | else 42 | return doubleByteOpCode[ReadByte()]; 43 | } 44 | 45 | #if NET6_0_OR_GREATER 46 | [RequiresUnreferencedCode(Constants.TrimWarning)] 47 | #endif 48 | MemberInfo? ReadOperand(OpCode code, MethodBase methodInfo) 49 | { 50 | MetadataToken = 0; 51 | int inlineLength; 52 | switch (code.OperandType) 53 | { 54 | case OperandType.InlineMethod: 55 | MetadataToken = ReadInt(); 56 | Type[]? methodArgs = null; 57 | if (methodInfo.GetType() != typeof(ConstructorInfo) && !methodInfo.GetType().IsSubclassOf(typeof(ConstructorInfo))) 58 | { 59 | methodArgs = methodInfo.GetGenericArguments(); 60 | } 61 | Type[]? typeArgs = null; 62 | if (methodInfo.DeclaringType != null) 63 | { 64 | typeArgs = methodInfo.DeclaringType.GetGenericArguments(); 65 | } 66 | try 67 | { 68 | return methodInfo.Module.ResolveMember(MetadataToken, typeArgs, methodArgs); 69 | } 70 | catch 71 | { 72 | // Can return System.ArgumentException : Token xxx is not a valid MemberInfo token in the scope of module xxx.dll 73 | return null; 74 | } 75 | 76 | case OperandType.InlineNone: 77 | inlineLength = 0; 78 | break; 79 | 80 | case OperandType.ShortInlineBrTarget: 81 | case OperandType.ShortInlineVar: 82 | case OperandType.ShortInlineI: 83 | inlineLength = 1; 84 | break; 85 | 86 | case OperandType.InlineVar: 87 | inlineLength = 2; 88 | break; 89 | 90 | case OperandType.InlineBrTarget: 91 | case OperandType.InlineField: 92 | case OperandType.InlineI: 93 | case OperandType.InlineString: 94 | case OperandType.InlineSig: 95 | case OperandType.InlineSwitch: 96 | case OperandType.InlineTok: 97 | case OperandType.InlineType: 98 | case OperandType.ShortInlineR: 99 | inlineLength = 4; 100 | break; 101 | 102 | case OperandType.InlineI8: 103 | case OperandType.InlineR: 104 | inlineLength = 8; 105 | break; 106 | 107 | default: 108 | return null; 109 | } 110 | 111 | for (var i = 0; i < inlineLength; i++) 112 | { 113 | ReadByte(); 114 | } 115 | 116 | return null; 117 | } 118 | 119 | byte ReadByte() => _cil[ptr++]; 120 | 121 | int ReadInt() 122 | { 123 | var b1 = ReadByte(); 124 | var b2 = ReadByte(); 125 | var b3 = ReadByte(); 126 | var b4 = ReadByte(); 127 | return b1 | b2 << 8 | b3 << 16 | b4 << 24; 128 | } 129 | 130 | static ILReader() 131 | { 132 | singleByteOpCode = new OpCode[225]; 133 | doubleByteOpCode = new OpCode[31]; 134 | 135 | var fields = GetOpCodeFields(); 136 | 137 | for (var i = 0; i < fields.Length; i++) 138 | { 139 | var code = (OpCode)fields[i].GetValue(null)!; 140 | if (code.OpCodeType == OpCodeType.Nternal) 141 | continue; 142 | 143 | if (code.Size == 1) 144 | singleByteOpCode[code.Value] = code; 145 | else 146 | doubleByteOpCode[code.Value & 0xff] = code; 147 | } 148 | } 149 | 150 | static FieldInfo[] GetOpCodeFields() => typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 8 | using System.IO; 9 | using System.Text; 10 | using Ben.Demystifier; 11 | 12 | namespace System.Diagnostics 13 | { 14 | public partial class EnhancedStackTrace : StackTrace, IEnumerable 15 | { 16 | #if NET6_0_OR_GREATER 17 | [RequiresUnreferencedCode(Constants.TrimWarning)] 18 | #endif 19 | public static EnhancedStackTrace Current() => new EnhancedStackTrace(new StackTrace(1 /* skip this one frame */, true)); 20 | 21 | private readonly List _frames; 22 | 23 | // Summary: 24 | // Initializes a new instance of the System.Diagnostics.StackTrace class using the 25 | // provided exception object. 26 | // 27 | // Parameters: 28 | // e: 29 | // The exception object from which to construct the stack trace. 30 | // 31 | // Exceptions: 32 | // T:System.ArgumentNullException: 33 | // The parameter e is null. 34 | #if NET6_0_OR_GREATER 35 | [RequiresUnreferencedCode(Constants.TrimWarning)] 36 | #endif 37 | public EnhancedStackTrace(Exception e) 38 | { 39 | if (e == null) 40 | { 41 | throw new ArgumentNullException(nameof(e)); 42 | } 43 | 44 | _frames = GetFrames(e); 45 | } 46 | 47 | #if NET6_0_OR_GREATER 48 | [RequiresUnreferencedCode(Constants.TrimWarning)] 49 | #endif 50 | public EnhancedStackTrace(StackTrace stackTrace) 51 | { 52 | if (stackTrace == null) 53 | { 54 | throw new ArgumentNullException(nameof(stackTrace)); 55 | } 56 | 57 | _frames = GetFrames(stackTrace); 58 | } 59 | 60 | /// 61 | /// Gets the number of frames in the stack trace. 62 | /// 63 | /// The number of frames in the stack trace. 64 | public override int FrameCount => _frames.Count; 65 | 66 | /// 67 | /// Gets the specified stack frame. 68 | /// 69 | /// The index of the stack frame requested. 70 | /// The specified stack frame. 71 | public override StackFrame GetFrame(int index) => _frames[index]; 72 | 73 | /// 74 | /// Returns a copy of all stack frames in the current stack trace. 75 | /// 76 | /// 77 | /// An array of type System.Diagnostics.StackFrame representing the function calls 78 | /// in the stack trace. 79 | /// 80 | public override StackFrame[] GetFrames() => _frames.ToArray(); 81 | 82 | /// 83 | /// Builds a readable representation of the stack trace. 84 | /// 85 | /// A readable representation of the stack trace. 86 | public override string ToString() 87 | { 88 | if (_frames == null || _frames.Count == 0) return ""; 89 | 90 | var sb = new StringBuilder(); 91 | 92 | Append(sb); 93 | 94 | return sb.ToString(); 95 | } 96 | 97 | 98 | internal void Append(StringBuilder sb) 99 | { 100 | var frames = _frames; 101 | var count = frames.Count; 102 | 103 | for (var i = 0; i < count; i++) 104 | { 105 | if (i > 0) 106 | { 107 | sb.Append(Environment.NewLine); 108 | } 109 | 110 | var frame = frames[i]; 111 | 112 | sb.Append(" at "); 113 | frame.MethodInfo.Append(sb); 114 | 115 | if (frame.GetFileName() is {} fileName 116 | // IsNullOrEmpty alone wasn't enough to disable the null warning 117 | && !string.IsNullOrEmpty(fileName)) 118 | { 119 | sb.Append(" in "); 120 | sb.Append(TryGetFullPath(fileName)); 121 | 122 | } 123 | 124 | var lineNo = frame.GetFileLineNumber(); 125 | if (lineNo != 0) 126 | { 127 | sb.Append(":line "); 128 | sb.Append(lineNo); 129 | } 130 | } 131 | } 132 | 133 | EnumerableIList GetEnumerator() => EnumerableIList.Create(_frames); 134 | IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); 135 | IEnumerator IEnumerable.GetEnumerator() => _frames.GetEnumerator(); 136 | 137 | /// 138 | /// Tries to convert a given to a full path. 139 | /// Returns original value if the conversion isn't possible or a given path is relative. 140 | /// 141 | public static string TryGetFullPath(string filePath) 142 | { 143 | if (Uri.TryCreate(filePath, UriKind.Absolute, out var uri) && uri.IsFile) 144 | { 145 | return Uri.UnescapeDataString(uri.AbsolutePath); 146 | } 147 | 148 | return filePath; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /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 | #if (NETFRAMEWORK && !DEBUG) 81 | var expected = string.Join(string.Empty, 82 | "System.ArgumentException: Value does not fall within the expected range.", 83 | " at void Ben.Demystifier.Test.MethodTests.MethodWithLambda()+() => { }", 84 | " at void Ben.Demystifier.Test.MethodTests.DemistifiesMethodWithLambda()"); 85 | #else 86 | var expected = string.Join(string.Empty, 87 | "System.ArgumentException: Value does not fall within the expected range.", 88 | " at void Ben.Demystifier.Test.MethodTests.MethodWithLambda()+() => { }", 89 | " at void Ben.Demystifier.Test.MethodTests.MethodWithLambda()", 90 | " at void Ben.Demystifier.Test.MethodTests.DemistifiesMethodWithLambda()"); 91 | #endif 92 | 93 | Assert.Equal(expected, trace); 94 | } 95 | 96 | [Fact] 97 | public async Task DemistifiesMethodWithAsyncLambda() 98 | { 99 | Exception dex = null; 100 | try 101 | { 102 | await MethodWithAsyncLambda(); 103 | } 104 | catch (Exception e) 105 | { 106 | dex = e.Demystify(); 107 | } 108 | 109 | // Assert 110 | var stackTrace = dex.ToString(); 111 | stackTrace = LineEndingsHelper.RemoveLineEndings(stackTrace); 112 | var trace = string.Join(string.Empty, stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); 113 | 114 | var expected = string.Join(string.Empty, 115 | "System.ArgumentException: Value does not fall within the expected range.", 116 | " at async Task Ben.Demystifier.Test.MethodTests.MethodWithAsyncLambda()+(?) => { }", 117 | " at async Task Ben.Demystifier.Test.MethodTests.MethodWithAsyncLambda()", 118 | " at async Task Ben.Demystifier.Test.MethodTests.DemistifiesMethodWithAsyncLambda()"); 119 | 120 | Assert.Equal(expected, trace); 121 | } 122 | 123 | private bool MethodWithNullableInt(int? number) => throw new ArgumentException(); 124 | 125 | private bool MethodWithDynamic(dynamic value) => throw new ArgumentException(); 126 | 127 | private void MethodWithLambda() 128 | { 129 | Func action = () => throw new ArgumentException(); 130 | action(); 131 | } 132 | 133 | private async Task MethodWithAsyncLambda() 134 | { 135 | Func action = async () => 136 | { 137 | await Task.CompletedTask; 138 | throw new ArgumentException(); 139 | }; 140 | 141 | await action(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 6 | using System.IO; 7 | using System.Reflection; 8 | using System.Reflection.Metadata; 9 | using System.Reflection.Metadata.Ecma335; 10 | using System.Reflection.PortableExecutable; 11 | using Ben.Demystifier; 12 | 13 | namespace System.Diagnostics.Internal 14 | { 15 | // Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.StackTrace.Sources/StackFrame/PortablePdbReader.cs 16 | #if NET6_0_OR_GREATER 17 | [RequiresUnreferencedCode(Constants.TrimWarning)] 18 | #endif 19 | // Allow direct file system usage 20 | #pragma warning disable SN0001 21 | internal class PortablePdbReader : IDisposable 22 | { 23 | private readonly Dictionary _cache = 24 | new Dictionary(StringComparer.Ordinal); 25 | 26 | #if NET6_0_OR_GREATER 27 | [UnconditionalSuppressMessage("SingleFile", "IL3000: Avoid accessing Assembly file path", Justification = Constants.SingleFileFallback)] 28 | #endif 29 | public void PopulateStackFrame(StackFrame frameInfo, MethodBase method, int IlOffset, out string fileName, out int row, out int column) 30 | { 31 | fileName = ""; 32 | row = 0; 33 | column = 0; 34 | 35 | if (method.Module.Assembly.IsDynamic) 36 | { 37 | return; 38 | } 39 | 40 | var metadataReader = GetMetadataReader(method.Module.Assembly.Location); 41 | 42 | if (metadataReader == null) 43 | { 44 | return; 45 | } 46 | 47 | var methodToken = MetadataTokens.Handle(method.MetadataToken); 48 | // Sometimes we get a HandleKind.ModuleDefinition. We simply don't populate those stack frames. 49 | if (methodToken.Kind != HandleKind.MethodDefinition) 50 | { 51 | return; 52 | } 53 | var handle = ((MethodDefinitionHandle)methodToken).ToDebugInformationHandle(); 54 | 55 | if (!handle.IsNil) 56 | { 57 | var methodDebugInfo = metadataReader.GetMethodDebugInformation(handle); 58 | var sequencePoints = methodDebugInfo.GetSequencePoints(); 59 | SequencePoint? bestPointSoFar = null; 60 | 61 | foreach (var point in sequencePoints) 62 | { 63 | if (point.Offset > IlOffset) 64 | { 65 | break; 66 | } 67 | 68 | if (point.StartLine != SequencePoint.HiddenLine) 69 | { 70 | bestPointSoFar = point; 71 | } 72 | } 73 | 74 | if (bestPointSoFar.HasValue) 75 | { 76 | row = bestPointSoFar.Value.StartLine; 77 | column = bestPointSoFar.Value.StartColumn; 78 | fileName = metadataReader.GetString(metadataReader.GetDocument(bestPointSoFar.Value.Document).Name); 79 | } 80 | } 81 | } 82 | 83 | private MetadataReader? GetMetadataReader(string assemblyPath) 84 | { 85 | if (_cache.TryGetValue(assemblyPath, out var provider)) 86 | { 87 | return provider?.GetMetadataReader(); 88 | } 89 | 90 | var pdbPath = GetPdbPath(assemblyPath); 91 | 92 | if (!string.IsNullOrEmpty(pdbPath) && File.Exists(pdbPath) && IsPortable(pdbPath!)) 93 | { 94 | var pdbStream = File.OpenRead(pdbPath); 95 | provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); 96 | } 97 | 98 | _cache[assemblyPath] = provider!; 99 | 100 | return provider?.GetMetadataReader(); 101 | } 102 | 103 | private static string? GetPdbPath(string assemblyPath) 104 | { 105 | if (string.IsNullOrEmpty(assemblyPath)) 106 | { 107 | return null; 108 | } 109 | 110 | if (File.Exists(assemblyPath)) 111 | { 112 | var peStream = File.OpenRead(assemblyPath); 113 | 114 | using var peReader = new PEReader(peStream); 115 | foreach (var entry in peReader.ReadDebugDirectory()) 116 | { 117 | if (entry.Type == DebugDirectoryEntryType.CodeView) 118 | { 119 | var codeViewData = peReader.ReadCodeViewDebugDirectoryData(entry); 120 | var peDirectory = Path.GetDirectoryName(assemblyPath); 121 | return peDirectory is null 122 | ? null 123 | : Path.Combine(peDirectory, Path.GetFileName(codeViewData.Path)); 124 | } 125 | } 126 | } 127 | 128 | return null; 129 | } 130 | 131 | private static bool IsPortable(string pdbPath) 132 | { 133 | using var pdbStream = File.OpenRead(pdbPath); 134 | return pdbStream.ReadByte() == 'B' && 135 | pdbStream.ReadByte() == 'S' && 136 | pdbStream.ReadByte() == 'J' && 137 | pdbStream.ReadByte() == 'B'; 138 | } 139 | 140 | public void Dispose() 141 | { 142 | foreach (var entry in _cache) 143 | { 144 | entry.Value?.Dispose(); 145 | } 146 | 147 | _cache.Clear(); 148 | } 149 | } 150 | #pragma warning restore SN0001 151 | } 152 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 6 | using System.Reflection; 7 | using System.Text; 8 | using Ben.Demystifier; 9 | 10 | namespace System.Diagnostics 11 | { 12 | #if NET6_0_OR_GREATER 13 | [RequiresUnreferencedCode(Constants.TrimWarning)] 14 | #endif 15 | public class ResolvedMethod 16 | { 17 | public MethodBase? MethodBase { get; set; } 18 | 19 | public Type? DeclaringType { get; set; } 20 | 21 | public bool IsAsync { get; set; } 22 | 23 | public bool IsLambda { get; set; } 24 | 25 | public ResolvedParameter? ReturnParameter { get; set; } 26 | 27 | public string? Name { get; set; } 28 | 29 | public int? Ordinal { get; set; } 30 | 31 | public string? GenericArguments { get; set; } 32 | 33 | public Type[]? ResolvedGenericArguments { get; set; } 34 | 35 | public MethodBase? SubMethodBase { get; set; } 36 | 37 | public string? SubMethod { get; set; } 38 | 39 | public EnumerableIList Parameters { get; set; } 40 | 41 | public EnumerableIList SubMethodParameters { get; set; } 42 | public int RecurseCount { get; internal set; } 43 | 44 | internal bool IsSequentialEquivalent(ResolvedMethod obj) 45 | { 46 | return 47 | IsAsync == obj.IsAsync && 48 | DeclaringType == obj.DeclaringType && 49 | Name == obj.Name && 50 | IsLambda == obj.IsLambda && 51 | Ordinal == obj.Ordinal && 52 | GenericArguments == obj.GenericArguments && 53 | SubMethod == obj.SubMethod; 54 | } 55 | 56 | public override string ToString() => Append(new StringBuilder()).ToString(); 57 | 58 | public StringBuilder Append(StringBuilder builder) 59 | => Append(builder, true); 60 | 61 | public StringBuilder Append(StringBuilder builder, bool fullName) 62 | { 63 | if (IsAsync) 64 | { 65 | builder.Append("async "); 66 | } 67 | 68 | if (ReturnParameter != null) 69 | { 70 | ReturnParameter.Append(builder); 71 | builder.Append(" "); 72 | } 73 | 74 | if (DeclaringType != null) 75 | { 76 | 77 | if (Name == ".ctor") 78 | { 79 | if (string.IsNullOrEmpty(SubMethod) && !IsLambda) 80 | builder.Append("new "); 81 | 82 | AppendDeclaringTypeName(builder, fullName); 83 | } 84 | else if (Name == ".cctor") 85 | { 86 | builder.Append("static "); 87 | AppendDeclaringTypeName(builder, fullName); 88 | } 89 | else 90 | { 91 | AppendDeclaringTypeName(builder, fullName) 92 | .Append(".") 93 | .Append(Name); 94 | } 95 | } 96 | else 97 | { 98 | builder.Append(Name); 99 | } 100 | builder.Append(GenericArguments); 101 | 102 | builder.Append("("); 103 | if (MethodBase != null) 104 | { 105 | var isFirst = true; 106 | foreach(var param in Parameters) 107 | { 108 | if (isFirst) 109 | { 110 | isFirst = false; 111 | } 112 | else 113 | { 114 | builder.Append(", "); 115 | } 116 | param.Append(builder); 117 | } 118 | } 119 | else 120 | { 121 | builder.Append("?"); 122 | } 123 | builder.Append(")"); 124 | 125 | if (!string.IsNullOrEmpty(SubMethod) || IsLambda) 126 | { 127 | builder.Append("+"); 128 | builder.Append(SubMethod); 129 | builder.Append("("); 130 | if (SubMethodBase != null) 131 | { 132 | var isFirst = true; 133 | foreach (var param in SubMethodParameters) 134 | { 135 | if (isFirst) 136 | { 137 | isFirst = false; 138 | } 139 | else 140 | { 141 | builder.Append(", "); 142 | } 143 | param.Append(builder); 144 | } 145 | } 146 | else 147 | { 148 | builder.Append("?"); 149 | } 150 | builder.Append(")"); 151 | if (IsLambda) 152 | { 153 | builder.Append(" => { }"); 154 | 155 | if (Ordinal.HasValue) 156 | { 157 | builder.Append(" ["); 158 | builder.Append(Ordinal); 159 | builder.Append("]"); 160 | } 161 | } 162 | } 163 | 164 | if (RecurseCount > 0) 165 | { 166 | builder.Append($" x {RecurseCount + 1:0}"); 167 | } 168 | 169 | return builder; 170 | } 171 | 172 | private StringBuilder AppendDeclaringTypeName(StringBuilder builder, bool fullName = true) 173 | { 174 | return DeclaringType != null ? builder.AppendTypeDisplayName(DeclaringType, fullName: fullName, includeGenericParameterNames: true) : builder; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.Diagnostics.CodeAnalysis; 6 | using System.Text; 7 | using Ben.Demystifier; 8 | 9 | namespace System.Diagnostics 10 | { 11 | // Adapted from https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.TypeNameHelper.Sources/TypeNameHelper.cs 12 | #if NET6_0_OR_GREATER 13 | [RequiresUnreferencedCode(Constants.TrimWarning)] 14 | #endif 15 | public static class TypeNameHelper 16 | { 17 | public static readonly Dictionary BuiltInTypeNames = new Dictionary 18 | { 19 | { typeof(void), "void" }, 20 | { typeof(bool), "bool" }, 21 | { typeof(byte), "byte" }, 22 | { typeof(char), "char" }, 23 | { typeof(decimal), "decimal" }, 24 | { typeof(double), "double" }, 25 | { typeof(float), "float" }, 26 | { typeof(int), "int" }, 27 | { typeof(long), "long" }, 28 | { typeof(object), "object" }, 29 | { typeof(sbyte), "sbyte" }, 30 | { typeof(short), "short" }, 31 | { typeof(string), "string" }, 32 | { typeof(uint), "uint" }, 33 | { typeof(ulong), "ulong" }, 34 | { typeof(ushort), "ushort" } 35 | }; 36 | 37 | public static readonly Dictionary FSharpTypeNames = new Dictionary 38 | { 39 | { "Unit", "void" }, 40 | { "FSharpOption", "Option" }, 41 | { "FSharpAsync", "Async" }, 42 | { "FSharpOption`1", "Option" }, 43 | { "FSharpAsync`1", "Async" } 44 | }; 45 | 46 | /// 47 | /// Pretty print a type name. 48 | /// 49 | /// The . 50 | /// true to print a fully qualified name. 51 | /// true to include generic parameter names. 52 | /// The pretty printed type name. 53 | public static string GetTypeDisplayName(Type type, bool fullName = true, bool includeGenericParameterNames = false) 54 | { 55 | var builder = new StringBuilder(); 56 | ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames)); 57 | return builder.ToString(); 58 | } 59 | 60 | public static StringBuilder AppendTypeDisplayName(this StringBuilder builder, Type type, bool fullName = true, bool includeGenericParameterNames = false) 61 | { 62 | ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames)); 63 | return builder; 64 | } 65 | 66 | /// 67 | /// Returns a name of given generic type without '`'. 68 | /// 69 | public static string GetTypeNameForGenericType(Type type) 70 | { 71 | if (!type.IsGenericType) 72 | { 73 | throw new ArgumentException("The given type should be generic", nameof(type)); 74 | } 75 | 76 | var genericPartIndex = type.Name.IndexOf('`'); 77 | 78 | return (genericPartIndex >= 0) ? type.Name.Substring(0, genericPartIndex) : type.Name; 79 | } 80 | 81 | #if NET6_0_OR_GREATER 82 | [UnconditionalSuppressMessage("SingleFile", "IL3002: calling members marked with 'RequiresAssemblyFilesAttribute'", Justification = Constants.SingleFileFallback)] 83 | #endif 84 | private static void ProcessType(StringBuilder builder, Type type, DisplayNameOptions options) 85 | { 86 | if (type.IsGenericType) 87 | { 88 | var underlyingType = Nullable.GetUnderlyingType(type); 89 | if (underlyingType != null) 90 | { 91 | ProcessType(builder, underlyingType, options); 92 | builder.Append('?'); 93 | } 94 | else 95 | { 96 | var genericArguments = type.GetGenericArguments(); 97 | ProcessGenericType(builder, type, genericArguments, genericArguments.Length, options); 98 | } 99 | } 100 | else if (type.IsArray) 101 | { 102 | ProcessArrayType(builder, type, options); 103 | } 104 | else if (BuiltInTypeNames.TryGetValue(type, out var builtInName)) 105 | { 106 | builder.Append(builtInName); 107 | } 108 | else if (type.Namespace == nameof(System)) 109 | { 110 | builder.Append(type.Name); 111 | } 112 | else if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll" 113 | && FSharpTypeNames.TryGetValue(type.Name, out builtInName)) 114 | { 115 | builder.Append(builtInName); 116 | } 117 | else if (type.IsGenericParameter) 118 | { 119 | if (options.IncludeGenericParameterNames) 120 | { 121 | builder.Append(type.Name); 122 | } 123 | } 124 | else 125 | { 126 | builder.Append(options.FullName ? type.FullName ?? type.Name : type.Name); 127 | } 128 | } 129 | 130 | private static void ProcessArrayType(StringBuilder builder, Type type, DisplayNameOptions options) 131 | { 132 | var innerType = type; 133 | while (innerType.IsArray) 134 | { 135 | if (innerType.GetElementType() is { } inner) 136 | { 137 | innerType = inner; 138 | } 139 | } 140 | 141 | ProcessType(builder, innerType, options); 142 | 143 | while (type.IsArray) 144 | { 145 | builder.Append('['); 146 | builder.Append(',', type.GetArrayRank() - 1); 147 | builder.Append(']'); 148 | if (type.GetElementType() is not { } elementType) 149 | { 150 | break; 151 | } 152 | type = elementType; 153 | } 154 | } 155 | 156 | #if NET6_0_OR_GREATER 157 | [UnconditionalSuppressMessage("SingleFile", "IL3002: calling members marked with 'RequiresAssemblyFilesAttribute'", Justification = Constants.SingleFileFallback)] 158 | #endif 159 | private static void ProcessGenericType(StringBuilder builder, Type type, Type[] genericArguments, int length, DisplayNameOptions options) 160 | { 161 | var offset = 0; 162 | if (type.IsNested && type.DeclaringType is not null) 163 | { 164 | offset = type.DeclaringType.GetGenericArguments().Length; 165 | } 166 | 167 | if (options.FullName) 168 | { 169 | if (type.IsNested && type.DeclaringType is not null) 170 | { 171 | ProcessGenericType(builder, type.DeclaringType, genericArguments, offset, options); 172 | builder.Append('+'); 173 | } 174 | else if (!string.IsNullOrEmpty(type.Namespace)) 175 | { 176 | builder.Append(type.Namespace); 177 | builder.Append('.'); 178 | } 179 | } 180 | 181 | var genericPartIndex = type.Name.IndexOf('`'); 182 | if (genericPartIndex <= 0) 183 | { 184 | builder.Append(type.Name); 185 | return; 186 | } 187 | 188 | if (type.Assembly.ManifestModule.Name == "FSharp.Core.dll" 189 | && FSharpTypeNames.TryGetValue(type.Name, out var builtInName)) 190 | { 191 | builder.Append(builtInName); 192 | } 193 | else 194 | { 195 | builder.Append(type.Name, 0, genericPartIndex); 196 | } 197 | 198 | builder.Append('<'); 199 | for (var i = offset; i < length; i++) 200 | { 201 | ProcessType(builder, genericArguments[i], options); 202 | if (i + 1 == length) 203 | { 204 | continue; 205 | } 206 | 207 | builder.Append(','); 208 | if (options.IncludeGenericParameterNames || !genericArguments[i + 1].IsGenericParameter) 209 | { 210 | builder.Append(' '); 211 | } 212 | } 213 | builder.Append('>'); 214 | } 215 | 216 | private struct DisplayNameOptions 217 | { 218 | public DisplayNameOptions(bool fullName, bool includeGenericParameterNames) 219 | { 220 | FullName = fullName; 221 | IncludeGenericParameterNames = includeGenericParameterNames; 222 | } 223 | 224 | public bool FullName { get; } 225 | 226 | public bool IncludeGenericParameterNames { get; } 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ben.Demystifier 2 | [![NuGet version (Ben.Demystifier)](https://img.shields.io/nuget/v/Ben.Demystifier.svg?style=flat-square)](https://www.nuget.org/packages/Ben.Demystifier/) 3 | [![build](https://github.com/benaadams/Ben.Demystifier/workflows/Demystifier%20PR%20Build/badge.svg)](https://github.com/benaadams/Ben.Demystifier/actions) 4 | 5 | Output the modern C# 7.0+ features in stack traces that looks like the C# source code that generated them rather than IL formatted. 6 | 7 | ## High performance understanding for stack traces 8 | 9 | .NET stack traces output the compiler transformed methods; rather than the source code methods, which make them slow to mentally parse and match back to the source code. 10 | 11 | The current output was good for C# 1.0; but has become progressively worse since C# 2.0 (iterators, generics) as new features are added to the .NET languages and at C# 7.1 the stack traces are esoteric (see: [Problems with current stack traces](#problems-with-current-stack-traces)). 12 | 13 | ### Make error logs more productive 14 | 15 | Output the modern C# 7.0+ features in stack traces in an understandable fashion that looks like the C# source code that generated them. 16 | 17 | [![Demystified stacktrace](https://aoa.blob.core.windows.net/aspnet/stacktrace-demystified.png)](https://aoa.blob.core.windows.net/aspnet/stacktrace-demystified.png) 18 | 19 | ### Usage 20 | 21 | ``` 22 | exception.Demystify() 23 | ``` 24 | Or instead of Environment.StackTrace 25 | ``` 26 | EnhancedStackTrace.Current() 27 | ``` 28 | Resolves the stack back to the C# source format of the calls (and is an inspectable list of stack frames) 29 | 30 | Calling `.ToString()` on the Demystified exception will produce a string stacktrace similar to the following (without the comments): 31 | 32 | ```csharp 33 | System.InvalidOperationException: Collection was modified; enumeration operation may not execute. 34 | at bool System.Collections.Generic.List+Enumerator.MoveNextRare() 35 | at IEnumerable Program.Iterator(int startAt)+MoveNext() // Resolved enumerator 36 | at bool System.Linq.Enumerable+SelectEnumerableIterator.MoveNext() // Resolved enumerator 37 | at string string.Join(string separator, IEnumerable values) 38 | at string Program+GenericClass.GenericMethod(ref TSubType value) 39 | at async Task Program.MethodAsync(int value) // Resolved async 40 | at async Task Program.MethodAsync(TValue value) // Resolved async 41 | at string Program.Method(string value)+()=>{} [0] // lambda source + ordinal 42 | at string Program.Method(string value)+()=>{} [1] // lambda source + ordinal 43 | at string Program.RunLambda(Func lambda) 44 | at (string val, bool) Program.Method(string value) // Tuple returning 45 | at ref string Program.RefMethod(in string value)+LocalFuncRefReturn() // ref return local func 46 | at int Program.RefMethod(in string value)+LocalFuncParam(string val) // local function 47 | at string Program.RefMethod(in string value) // in param (readonly ref) 48 | at (string val, bool) static Program()+(string s, bool b)=>{} // tuple return static lambda 49 | at void static Program()+(string s, bool b)=>{} // void static lambda 50 | at void Program.Start((string val, bool) param) // Resolved tuple param 51 | at void Program.Start((string val, bool) param)+LocalFunc1(long l) // void local function 52 | at bool Program.Start((string val, bool) param)+LocalFunc2(bool b1, bool b2) // bool return local function 53 | at string Program.Start() 54 | at void Program()+()=>{} // ctor defined lambda 55 | at void Program(Action action)+(object state)=>{} // ctor defined lambda 56 | at void Program.RunAction(Action lambda, object state) 57 | at new Program(Action action) // constructor 58 | at new Program() // constructor 59 | at void Program.Main(String[] args) 60 | ``` 61 | 62 | Calling `.ToString()` on the same exception would produce the following output 63 | 64 | ```csharp 65 | System.InvalidOperationException: Collection was modified; enumeration operation may not execute. 66 | at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion() // ? low value 67 | at System.Collections.Generic.List`1.Enumerator.MoveNextRare() 68 | at Program.d__3.MoveNext() // which enumerator? 69 | at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext() // which enumerator? 70 | at System.String.Join(String separator, IEnumerable`1 values) 71 | at Program.GenericClass`1.GenericMethod[TSubType](TSubType& value) 72 | at Program.d__4.MoveNext() // which async overload? 73 | --- End of stack trace from previous location where exception was thrown --- // ? no value 74 | at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // ? no value 75 | at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // ? no value 76 | at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() // ? no value 77 | at Program.d__5`1.MoveNext() // which async overload? 78 | --- End of stack trace from previous location where exception was thrown --- // ? no value 79 | at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // ? no value 80 | at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // ? no value 81 | at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() // ? no value 82 | at Program.<>c__DisplayClass8_0.b__0() // ¯\_(ツ)_/¯ 83 | at Program.<>c__DisplayClass8_0.b__1() // ¯\_(ツ)_/¯ 84 | at Program.RunLambda(Func`1 lambda) 85 | at Program.Method(String value) 86 | at Program.g__LocalFuncRefReturn|10_1(<>c__DisplayClass10_0& ) // local function 87 | at Program.g__LocalFuncParam|10_0(String val, <>c__DisplayClass10_0& ) // local function 88 | at Program.RefMethod(String value) 89 | at Program.<>c.<.cctor>b__18_1(String s, Boolean b) // ¯\_(ツ)_/¯ 90 | at Program.<>c.<.cctor>b__18_0(String s, Boolean b) // ¯\_(ツ)_/¯ 91 | at Program.Start(ValueTuple`2 param) // Tuple param? 92 | at Program.g__LocalFunc1|11_0(Int64 l) // local function 93 | at Program.g__LocalFunc2|11_1(Boolean b1, Boolean b2) // local function 94 | at Program.Start() 95 | at Program.<>c.<.ctor>b__1_0() // ¯\_(ツ)_/¯ 96 | at Program.<>c__DisplayClass2_0.<.ctor>b__0(Object state) // ¯\_(ツ)_/¯ 97 | at Program.RunAction(Action`1 lambda, Object state) 98 | at Program..ctor(Action action) // constructor 99 | at Program..ctor() // constructor 100 | at Program.Main(String[] args) 101 | ``` 102 | Which is far less helpful, and close to jibberish in places 103 | 104 | 105 | ### Problems with current stack traces: 106 | 107 | * **constructors** 108 | 109 | Does not match code, output as `.ctor` and `.cctor` 110 | 111 | * **parameters** 112 | 113 | Do not specify qualifier `ref`, `out` or `in` 114 | 115 | * **iterators** 116 | 117 | Cannot determine overload `d__3.MoveNext()` rather than `Iterator(int startAt)+MoveNext()` 118 | 119 | * **Linq** 120 | 121 | Cannot determine overload 122 | 123 | `Linq.Enumerable.SelectEnumerableIterator``2.MoveNext()` 124 | 125 | rather than 126 | 127 | `Linq.Enumerable+SelectEnumerableIterator.MoveNext()` 128 | 129 | * **async** 130 | 131 | Cannot determine overload and no modifier such as `async` 132 | 133 | `d__5``1.MoveNext()` 134 | 135 | rather than 136 | 137 | `async Task Program.MethodAsync(int value)` 138 | 139 | Noise! 140 | ``` 141 | --- End of stack trace from previous location where exception was thrown --- 142 | at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() 143 | at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 144 | at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 145 | at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task) 146 | at System.Runtime.CompilerServices.TaskAwaiter.GetResult() 147 | ``` 148 | 149 | * **lambdas** 150 | 151 | 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. 152 | 153 | * **local functions** 154 | 155 | 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. 156 | 157 | * **generic parameters** 158 | 159 | Not resolved, only an indication of the number `RunLambda(Func``1 lambda)` rather than `RunLambda(Func lambda)` 160 | 161 | * **value tuples** 162 | 163 | Do not match code, output as `ValueTuple``2 param` rather than `(string val, bool) param` 164 | 165 | * **primitive types** 166 | 167 | Do not match code, output as `Int64`, `Boolean`, `String` rather than `long`, `bool`, `string` 168 | 169 | * **return types** 170 | 171 | Skipped entirely from method signature 172 | 173 | ### Benchmarks 174 | 175 | To run benchmarks from the repository root: 176 | ``` 177 | dotnet run -p .\test\Ben.Demystifier.Benchmarks\ -c Release -f net6.0 All 178 | ``` 179 | Note: we're only kicking off via `net8.0`, benchmarks will run for all configured platforms like `net462`. 180 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.CodeAnalysis; 10 | using System.Diagnostics.Internal; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Runtime.CompilerServices; 14 | using System.Runtime.ExceptionServices; 15 | using System.Text; 16 | using System.Threading; 17 | using System.Threading.Tasks; 18 | using Ben.Demystifier; 19 | 20 | namespace System.Diagnostics 21 | { 22 | #if NET6_0_OR_GREATER 23 | [RequiresUnreferencedCode(Constants.TrimWarning)] 24 | #endif 25 | public partial class EnhancedStackTrace 26 | { 27 | private static readonly Type? StackTraceHiddenAttributeType = Type.GetType("System.Diagnostics.StackTraceHiddenAttribute", false); 28 | private static readonly Type? AsyncIteratorStateMachineAttributeType = Type.GetType("System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", false); 29 | 30 | #if NET6_0_OR_GREATER 31 | [UnconditionalSuppressMessage("Trimming", "IL2026: RequiresUnreferencedCode", Justification = Constants.AvoidAtRuntime)] 32 | #endif 33 | static EnhancedStackTrace() 34 | { 35 | if (AsyncIteratorStateMachineAttributeType != null) return; 36 | 37 | Assembly mba; 38 | try 39 | { 40 | mba = Assembly.Load("Microsoft.Bcl.AsyncInterfaces"); 41 | AsyncIteratorStateMachineAttributeType = mba.GetType("System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute", false); 42 | } 43 | catch 44 | { 45 | return; 46 | } 47 | 48 | } 49 | 50 | #if NET6_0_OR_GREATER 51 | [RequiresUnreferencedCode(Constants.TrimWarning)] 52 | #endif 53 | private static List GetFrames(Exception exception) 54 | { 55 | if (exception == null) 56 | { 57 | return new List(); 58 | } 59 | 60 | var needFileInfo = true; 61 | var stackTrace = new StackTrace(exception, needFileInfo); 62 | 63 | return GetFrames(stackTrace); 64 | } 65 | 66 | #if NET6_0_OR_GREATER 67 | [RequiresUnreferencedCode(Constants.TrimWarning)] 68 | #endif 69 | public static List GetFrames(StackTrace stackTrace) 70 | { 71 | var frames = new List(); 72 | var stackFrames = stackTrace.GetFrames(); 73 | 74 | if (stackFrames == null) 75 | { 76 | return frames; 77 | } 78 | 79 | EnhancedStackFrame? lastFrame = null; 80 | PortablePdbReader? portablePdbReader = null; 81 | try 82 | { 83 | for (var i = 0; i < stackFrames.Length; i++) 84 | { 85 | var frame = stackFrames[i]; 86 | if (frame is null) 87 | { 88 | continue; 89 | } 90 | var method = frame.GetMethod(); 91 | 92 | // Always show last stackFrame 93 | if (method != null && !ShowInStackTrace(method) && i < stackFrames.Length - 1) 94 | { 95 | continue; 96 | } 97 | 98 | var fileName = frame.GetFileName(); 99 | var row = frame.GetFileLineNumber(); 100 | var column = frame.GetFileColumnNumber(); 101 | var ilOffset = frame.GetILOffset(); 102 | if (method != null && string.IsNullOrEmpty(fileName) && ilOffset >= 0) 103 | { 104 | // .NET Framework and older versions of mono don't support portable PDBs 105 | // so we read it manually to get file name and line information 106 | (portablePdbReader ??= new PortablePdbReader()).PopulateStackFrame(frame, method, frame.GetILOffset(), out fileName, out row, out column); 107 | } 108 | 109 | if (method is null) 110 | { 111 | // Method can't be null 112 | continue; 113 | } 114 | 115 | var resolvedMethod = GetMethodDisplayString(method); 116 | if (lastFrame?.IsEquivalent(resolvedMethod, fileName, row, column) ?? false) 117 | { 118 | lastFrame.IsRecursive = true; 119 | } 120 | else 121 | { 122 | var stackFrame = new EnhancedStackFrame(frame, resolvedMethod, fileName, row, column); 123 | frames.Add(stackFrame); 124 | lastFrame = stackFrame; 125 | } 126 | } 127 | } 128 | finally 129 | { 130 | portablePdbReader?.Dispose(); 131 | } 132 | 133 | return frames; 134 | } 135 | 136 | #if NET6_0_OR_GREATER 137 | [RequiresUnreferencedCode(Constants.TrimWarning)] 138 | #endif 139 | public static ResolvedMethod GetMethodDisplayString(MethodBase originMethod) 140 | { 141 | var method = originMethod; 142 | 143 | var methodDisplayInfo = new ResolvedMethod 144 | { 145 | SubMethodBase = method 146 | }; 147 | 148 | // Type name 149 | var type = method.DeclaringType; 150 | 151 | var subMethodName = method.Name; 152 | var methodName = method.Name; 153 | 154 | var isAsyncStateMachine = typeof(IAsyncStateMachine).IsAssignableFrom(type); 155 | if (isAsyncStateMachine || typeof(IEnumerator).IsAssignableFrom(type)) 156 | { 157 | methodDisplayInfo.IsAsync = isAsyncStateMachine; 158 | 159 | // Convert StateMachine methods to correct overload +MoveNext() 160 | if (!TryResolveStateMachineMethod(ref method, out type)) 161 | { 162 | methodDisplayInfo.SubMethodBase = null; 163 | subMethodName = null; 164 | } 165 | 166 | methodName = method.Name; 167 | } 168 | else if (IsFSharpAsync(method)) 169 | { 170 | methodDisplayInfo.IsAsync = true; 171 | methodDisplayInfo.SubMethodBase = null; 172 | subMethodName = null; 173 | methodName = null; 174 | } 175 | 176 | // Method name 177 | methodDisplayInfo.MethodBase = method; 178 | methodDisplayInfo.Name = methodName; 179 | if (method.Name.IndexOf("<") >= 0) 180 | { 181 | if (TryResolveGeneratedName(ref method, out type, out methodName, out subMethodName, out var kind, out var ordinal)) 182 | { 183 | methodName = method.Name; 184 | methodDisplayInfo.MethodBase = method; 185 | methodDisplayInfo.Name = methodName; 186 | methodDisplayInfo.Ordinal = ordinal; 187 | } 188 | else 189 | { 190 | methodDisplayInfo.MethodBase = null; 191 | } 192 | 193 | methodDisplayInfo.IsLambda = (kind == GeneratedNameKind.LambdaMethod); 194 | 195 | if (methodDisplayInfo.IsLambda && type != null) 196 | { 197 | if (methodName == ".cctor") 198 | { 199 | if (type.IsGenericTypeDefinition && !type.IsConstructedGenericType) 200 | { 201 | // TODO: diagnose type's generic type arguments from frame's "this" or something 202 | } 203 | else 204 | { 205 | var fields = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 206 | foreach (var field in fields) 207 | { 208 | var value = field.GetValue(field); 209 | if (value is Delegate d && d.Target is not null) 210 | { 211 | if (d.Method == originMethod && 212 | d.Target.ToString() == originMethod.DeclaringType?.ToString()) 213 | { 214 | methodDisplayInfo.Name = field.Name; 215 | methodDisplayInfo.IsLambda = false; 216 | method = originMethod; 217 | break; 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | if (subMethodName != methodName) 227 | { 228 | methodDisplayInfo.SubMethod = subMethodName; 229 | } 230 | 231 | // ResolveStateMachineMethod may have set declaringType to null 232 | if (type != null) 233 | { 234 | methodDisplayInfo.DeclaringType = type; 235 | } 236 | 237 | if (method is MethodInfo mi) 238 | { 239 | var returnParameter = mi.ReturnParameter; 240 | if (returnParameter != null) 241 | { 242 | methodDisplayInfo.ReturnParameter = GetParameter(mi.ReturnParameter); 243 | } 244 | else if (mi.ReturnType != null) 245 | { 246 | methodDisplayInfo.ReturnParameter = new ResolvedParameter(mi.ReturnType) 247 | { 248 | Prefix = "", 249 | Name = "", 250 | }; 251 | } 252 | } 253 | 254 | if (method.IsGenericMethod) 255 | { 256 | var genericArguments = method.GetGenericArguments(); 257 | var genericArgumentsString = string.Join(", ", genericArguments 258 | .Select(arg => TypeNameHelper.GetTypeDisplayName(arg, fullName: false, includeGenericParameterNames: true))); 259 | methodDisplayInfo.GenericArguments += "<" + genericArgumentsString + ">"; 260 | methodDisplayInfo.ResolvedGenericArguments = genericArguments; 261 | } 262 | 263 | // Method parameters 264 | var parameters = method.GetParameters(); 265 | if (parameters.Length > 0) 266 | { 267 | var parameterList = new List(parameters.Length); 268 | foreach (var parameter in parameters) 269 | { 270 | parameterList.Add(GetParameter(parameter)); 271 | } 272 | 273 | methodDisplayInfo.Parameters = parameterList; 274 | } 275 | 276 | if (methodDisplayInfo.SubMethodBase == methodDisplayInfo.MethodBase) 277 | { 278 | methodDisplayInfo.SubMethodBase = null; 279 | } 280 | else if (methodDisplayInfo.SubMethodBase != null) 281 | { 282 | parameters = methodDisplayInfo.SubMethodBase.GetParameters(); 283 | if (parameters.Length > 0) 284 | { 285 | var parameterList = new List(parameters.Length); 286 | foreach (var parameter in parameters) 287 | { 288 | var param = GetParameter(parameter); 289 | if (param.Name?.StartsWith("<") ?? true) continue; 290 | 291 | parameterList.Add(param); 292 | } 293 | 294 | methodDisplayInfo.SubMethodParameters = parameterList; 295 | } 296 | } 297 | 298 | return methodDisplayInfo; 299 | } 300 | 301 | private static bool IsFSharpAsync(MethodBase method) 302 | { 303 | if (method is MethodInfo minfo) 304 | { 305 | var returnType = minfo.ReturnType; 306 | if (returnType.Namespace == "Microsoft.FSharp.Control" && returnType.Name == "FSharpAsync`1") 307 | { 308 | return true; 309 | } 310 | } 311 | 312 | return false; 313 | } 314 | 315 | #if NET6_0_OR_GREATER 316 | [RequiresUnreferencedCode(Constants.TrimWarning)] 317 | #endif 318 | private static bool TryResolveGeneratedName(ref MethodBase method, out Type? type, out string methodName, out string? subMethodName, out GeneratedNameKind kind, out int? ordinal) 319 | { 320 | kind = GeneratedNameKind.None; 321 | type = method.DeclaringType; 322 | subMethodName = null; 323 | ordinal = null; 324 | methodName = method.Name; 325 | 326 | var generatedName = methodName; 327 | 328 | if (!TryParseGeneratedName(generatedName, out kind, out var openBracketOffset, out var closeBracketOffset)) 329 | { 330 | return false; 331 | } 332 | 333 | methodName = generatedName.Substring(openBracketOffset + 1, closeBracketOffset - openBracketOffset - 1); 334 | 335 | switch (kind) 336 | { 337 | case GeneratedNameKind.LocalFunction: 338 | { 339 | var localNameStart = generatedName.IndexOf((char)kind, closeBracketOffset + 1); 340 | if (localNameStart < 0) break; 341 | localNameStart += 3; 342 | 343 | if (localNameStart < generatedName.Length) 344 | { 345 | var localNameEnd = generatedName.IndexOf("|", localNameStart); 346 | if (localNameEnd > 0) 347 | { 348 | subMethodName = generatedName.Substring(localNameStart, localNameEnd - localNameStart); 349 | } 350 | } 351 | break; 352 | } 353 | case GeneratedNameKind.LambdaMethod: 354 | subMethodName = ""; 355 | break; 356 | } 357 | 358 | var dt = method.DeclaringType; 359 | if (dt == null) 360 | { 361 | return false; 362 | } 363 | 364 | var matchHint = GetMatchHint(kind, method); 365 | 366 | var matchName = methodName; 367 | 368 | var candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 369 | if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true; 370 | 371 | var candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 372 | if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true; 373 | 374 | const int MaxResolveDepth = 10; 375 | for (var i = 0; i < MaxResolveDepth; i++) 376 | { 377 | dt = dt.DeclaringType; 378 | if (dt == null) 379 | { 380 | return false; 381 | } 382 | 383 | candidateMethods = dt.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 384 | if (TryResolveSourceMethod(candidateMethods, kind, matchHint, ref method, ref type, out ordinal)) return true; 385 | 386 | candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 387 | if (TryResolveSourceMethod(candidateConstructors, kind, matchHint, ref method, ref type, out ordinal)) return true; 388 | 389 | if (methodName == ".cctor") 390 | { 391 | candidateConstructors = dt.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly).Where(m => m.Name == matchName); 392 | foreach (var cctor in candidateConstructors) 393 | { 394 | method = cctor; 395 | type = dt; 396 | return true; 397 | } 398 | } 399 | } 400 | 401 | return false; 402 | } 403 | 404 | #if NET6_0_OR_GREATER 405 | [RequiresUnreferencedCode(Constants.TrimWarning)] 406 | #endif 407 | private static bool TryResolveSourceMethod(IEnumerable candidateMethods, GeneratedNameKind kind, string? matchHint, ref MethodBase method, ref Type? type, out int? ordinal) 408 | { 409 | ordinal = null; 410 | foreach (var candidateMethod in candidateMethods) 411 | { 412 | MethodBody? methodBody = null; 413 | try 414 | { 415 | methodBody = candidateMethod.GetMethodBody(); 416 | } 417 | catch 418 | { 419 | // Platforms like native AOT don't provide access to IL method bodies 420 | } 421 | if (methodBody == null) 422 | { 423 | continue; 424 | } 425 | if (kind == GeneratedNameKind.LambdaMethod) 426 | { 427 | foreach (var v in EnumerableIList.Create(methodBody.LocalVariables)) 428 | { 429 | if (v.LocalType == type) 430 | { 431 | GetOrdinal(method, ref ordinal); 432 | 433 | } 434 | method = candidateMethod; 435 | type = method.DeclaringType; 436 | return true; 437 | } 438 | } 439 | 440 | try 441 | { 442 | var rawIl = methodBody.GetILAsByteArray(); 443 | if (rawIl is null) 444 | { 445 | continue; 446 | } 447 | var reader = new ILReader(rawIl); 448 | while (reader.Read(candidateMethod)) 449 | { 450 | if (reader.Operand is MethodBase mb) 451 | { 452 | if (method == mb || matchHint != null && method.Name.Contains(matchHint)) 453 | { 454 | if (kind == GeneratedNameKind.LambdaMethod) 455 | { 456 | GetOrdinal(method, ref ordinal); 457 | } 458 | 459 | method = candidateMethod; 460 | type = method.DeclaringType; 461 | return true; 462 | } 463 | } 464 | } 465 | } 466 | catch 467 | { 468 | // https://github.com/benaadams/Ben.Demystifier/issues/32 469 | // Skip methods where il can't be interpreted 470 | } 471 | } 472 | 473 | return false; 474 | } 475 | 476 | #if NET6_0_OR_GREATER 477 | [RequiresUnreferencedCode(Constants.TrimWarning)] 478 | #endif 479 | private static void GetOrdinal(MethodBase method, ref int? ordinal) 480 | { 481 | var lamdaStart = method.Name.IndexOf((char)GeneratedNameKind.LambdaMethod + "__") + 3; 482 | if (lamdaStart > 3) 483 | { 484 | var secondStart = method.Name.IndexOf("_", lamdaStart) + 1; 485 | if (secondStart > 0) 486 | { 487 | lamdaStart = secondStart; 488 | } 489 | 490 | if (!int.TryParse(method.Name.Substring(lamdaStart), out var foundOrdinal)) 491 | { 492 | ordinal = null; 493 | return; 494 | } 495 | 496 | ordinal = foundOrdinal; 497 | 498 | var methods = method.DeclaringType?.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); 499 | 500 | var count = 0; 501 | if (methods != null) 502 | { 503 | var startName = method.Name.Substring(0, lamdaStart); 504 | foreach (var m in methods) 505 | { 506 | if (m.Name.Length > lamdaStart && m.Name.StartsWith(startName)) 507 | { 508 | count++; 509 | 510 | if (count > 1) 511 | { 512 | break; 513 | } 514 | } 515 | } 516 | } 517 | 518 | if (count <= 1) 519 | { 520 | ordinal = null; 521 | } 522 | } 523 | } 524 | 525 | static string? GetMatchHint(GeneratedNameKind kind, MethodBase method) 526 | { 527 | var methodName = method.Name; 528 | 529 | switch (kind) 530 | { 531 | case GeneratedNameKind.LocalFunction: 532 | var start = methodName.IndexOf("|"); 533 | if (start < 1) return null; 534 | var end = methodName.IndexOf("_", start) + 1; 535 | if (end <= start) return null; 536 | 537 | return methodName.Substring(start, end - start); 538 | } 539 | return null; 540 | } 541 | 542 | // Parse the generated name. Returns true for names of the form 543 | // [CS$]<[middle]>c[__[suffix]] where [CS$] is included for certain 544 | // generated names, where [middle] and [__[suffix]] are optional, 545 | // and where c is a single character in [1-9a-z] 546 | // (csharp\LanguageAnalysis\LIB\SpecialName.cpp). 547 | internal static bool TryParseGeneratedName( 548 | string name, 549 | out GeneratedNameKind kind, 550 | out int openBracketOffset, 551 | out int closeBracketOffset) 552 | { 553 | openBracketOffset = -1; 554 | if (name.StartsWith("CS$<", StringComparison.Ordinal)) 555 | { 556 | openBracketOffset = 3; 557 | } 558 | else if (name.StartsWith("<", StringComparison.Ordinal)) 559 | { 560 | openBracketOffset = 0; 561 | } 562 | 563 | if (openBracketOffset >= 0) 564 | { 565 | closeBracketOffset = IndexOfBalancedParenthesis(name, openBracketOffset, '>'); 566 | if (closeBracketOffset >= 0 && closeBracketOffset + 1 < name.Length) 567 | { 568 | int c = name[closeBracketOffset + 1]; 569 | if ((c >= '1' && c <= '9') || (c >= 'a' && c <= 'z')) // Note '0' is not special. 570 | { 571 | kind = (GeneratedNameKind)c; 572 | return true; 573 | } 574 | } 575 | } 576 | 577 | kind = GeneratedNameKind.None; 578 | openBracketOffset = -1; 579 | closeBracketOffset = -1; 580 | return false; 581 | } 582 | 583 | 584 | private static int IndexOfBalancedParenthesis(string str, int openingOffset, char closing) 585 | { 586 | var opening = str[openingOffset]; 587 | 588 | var depth = 1; 589 | for (var i = openingOffset + 1; i < str.Length; i++) 590 | { 591 | var c = str[i]; 592 | if (c == opening) 593 | { 594 | depth++; 595 | } 596 | else if (c == closing) 597 | { 598 | depth--; 599 | if (depth == 0) 600 | { 601 | return i; 602 | } 603 | } 604 | } 605 | 606 | return -1; 607 | } 608 | 609 | private static string GetPrefix(ParameterInfo parameter) 610 | { 611 | if (Attribute.IsDefined(parameter, typeof(ParamArrayAttribute), false)) 612 | { 613 | return "params"; 614 | } 615 | 616 | if (parameter.IsOut) 617 | { 618 | return "out"; 619 | } 620 | 621 | if (parameter.IsIn) 622 | { 623 | return "in"; 624 | } 625 | 626 | if (parameter.ParameterType.IsByRef) 627 | { 628 | return "ref"; 629 | } 630 | 631 | return string.Empty; 632 | } 633 | 634 | #if NET6_0_OR_GREATER 635 | [RequiresUnreferencedCode(Constants.TrimWarning)] 636 | #endif 637 | private static ResolvedParameter GetParameter(ParameterInfo parameter) 638 | { 639 | var prefix = GetPrefix(parameter); 640 | var parameterType = parameter.ParameterType; 641 | 642 | if (parameterType.IsGenericType) 643 | { 644 | var customAttribs = parameter.GetCustomAttributes(inherit: false); 645 | 646 | #if NETFRAMEWORK 647 | var tupleNameAttribute = customAttribs.OfType().FirstOrDefault(a => a.IsTupleElementNameAttribute()); 648 | 649 | var tupleNames = tupleNameAttribute?.GetTransformerNames(); 650 | #else 651 | var tupleNameAttribute = customAttribs.OfType().FirstOrDefault(); 652 | 653 | var tupleNames = tupleNameAttribute?.TransformNames; 654 | #endif 655 | 656 | if (tupleNames?.Count > 0) 657 | { 658 | return GetValueTupleParameter(tupleNames, prefix, parameter.Name, parameterType); 659 | } 660 | } 661 | 662 | if (parameterType.IsByRef && parameterType.GetElementType() is {} elementType) 663 | { 664 | parameterType = elementType; 665 | } 666 | 667 | return new ResolvedParameter(parameterType) 668 | { 669 | Prefix = prefix, 670 | Name = parameter.Name, 671 | IsDynamicType = parameter.IsDefined(typeof(DynamicAttribute), false) 672 | }; 673 | } 674 | 675 | #if NET6_0_OR_GREATER 676 | [RequiresUnreferencedCode(Constants.TrimWarning)] 677 | #endif 678 | private static ResolvedParameter GetValueTupleParameter(IList tupleNames, string prefix, string? name, Type parameterType) 679 | { 680 | return new ValueTupleResolvedParameter(parameterType, tupleNames) 681 | { 682 | Prefix = prefix, 683 | Name = name, 684 | }; 685 | } 686 | 687 | private static string GetValueTupleParameterName(IList tupleNames, Type parameterType) 688 | { 689 | var sb = new StringBuilder(); 690 | sb.Append("("); 691 | var args = parameterType.GetGenericArguments(); 692 | for (var i = 0; i < args.Length; i++) 693 | { 694 | if (i > 0) 695 | { 696 | sb.Append(", "); 697 | } 698 | 699 | sb.Append(TypeNameHelper.GetTypeDisplayName(args[i], fullName: false, includeGenericParameterNames: true)); 700 | 701 | if (i >= tupleNames.Count) 702 | { 703 | continue; 704 | } 705 | 706 | var argName = tupleNames[i]; 707 | if (argName == null) 708 | { 709 | continue; 710 | } 711 | 712 | sb.Append(" "); 713 | sb.Append(argName); 714 | } 715 | 716 | sb.Append(")"); 717 | return sb.ToString(); 718 | } 719 | 720 | private static bool ShowInStackTrace(MethodBase method) 721 | { 722 | // Since .NET 5: 723 | // https://github.com/dotnet/runtime/blob/7c18d4d6488dab82124d475d1199def01d1d252c/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L348-L361 724 | if ((method.MethodImplementationFlags & MethodImplAttributes.AggressiveInlining) != 0) 725 | { 726 | // Aggressive Inlines won't normally show in the StackTrace; however for Tier0 Jit and 727 | // cross-assembly AoT/R2R these inlines will be blocked until Tier1 Jit re-Jits 728 | // them when they will inline. We don't show them in the StackTrace to bring consistency 729 | // between this first-pass asm and fully optimized asm. 730 | return false; 731 | } 732 | 733 | // Since .NET Core 2: 734 | if (StackTraceHiddenAttributeType != null) 735 | { 736 | // Don't show any methods marked with the StackTraceHiddenAttribute 737 | // https://github.com/dotnet/coreclr/pull/14652 738 | if (IsStackTraceHidden(method)) 739 | { 740 | return false; 741 | } 742 | } 743 | 744 | var type = method.DeclaringType; 745 | 746 | if (type == null) 747 | { 748 | return true; 749 | } 750 | 751 | // Since .NET Core 2: 752 | if (StackTraceHiddenAttributeType != null) 753 | { 754 | // Don't show any methods marked with the StackTraceHiddenAttribute 755 | // https://github.com/dotnet/coreclr/pull/14652 756 | if (IsStackTraceHidden(type)) 757 | { 758 | return false; 759 | } 760 | } 761 | 762 | if (type == typeof(Task<>) && method.Name == "InnerInvoke") 763 | { 764 | return false; 765 | } 766 | if (type == typeof(ValueTask<>) && method.Name == "get_Result") 767 | { 768 | return false; 769 | } 770 | if (method.Name.StartsWith("System.Threading.Tasks.Sources.IValueTaskSource") && method.Name.EndsWith(".GetResult")) 771 | { 772 | return false; 773 | } 774 | if (method.Name == "GetResult" && method.DeclaringType?.FullName == "System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore`1") 775 | { 776 | return false; 777 | } 778 | if (type == typeof(Task) || type.DeclaringType == typeof(Task)) 779 | { 780 | if (method.Name.Contains(".cctor")) 781 | { 782 | return false; 783 | } 784 | 785 | switch (method.Name) 786 | { 787 | case "ExecuteWithThreadLocal": 788 | case "Execute": 789 | case "ExecutionContextCallback": 790 | case "ExecuteEntry": 791 | case "InnerInvoke": 792 | case "ExecuteEntryUnsafe": 793 | case "ExecuteFromThreadPool": 794 | return false; 795 | } 796 | } 797 | if (type == typeof(ExecutionContext)) 798 | { 799 | if (method.Name.Contains(".cctor")) 800 | { 801 | return false; 802 | } 803 | 804 | switch (method.Name) 805 | { 806 | case "RunInternal": 807 | case "Run": 808 | case "RunFromThreadPoolDispatchLoop": 809 | return false; 810 | } 811 | } 812 | 813 | if (type.Namespace == "Microsoft.FSharp.Control") 814 | { 815 | switch (type.Name) 816 | { 817 | case "AsyncPrimitives": 818 | case "Trampoline": 819 | return false; 820 | case var typeName when type.IsGenericType: 821 | { 822 | if (typeName == "AsyncResult`1") return false; 823 | else break; 824 | } 825 | } 826 | } 827 | 828 | if (type.Namespace == "Ply") 829 | { 830 | if (type.DeclaringType?.Name == "TplPrimitives") 831 | { 832 | return false; 833 | } 834 | } 835 | 836 | // Fallbacks for runtime pre-StackTraceHiddenAttribute 837 | if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw") 838 | { 839 | return false; 840 | } 841 | 842 | if (type == typeof(TaskAwaiter) || 843 | type == typeof(TaskAwaiter<>) || 844 | type == typeof(ValueTaskAwaiter) || 845 | type == typeof(ValueTaskAwaiter<>) || 846 | type == typeof(ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter) || 847 | type == typeof(ConfiguredValueTaskAwaitable<>.ConfiguredValueTaskAwaiter) || 848 | type == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) || 849 | type == typeof(ConfiguredTaskAwaitable<>.ConfiguredTaskAwaiter)) 850 | { 851 | switch (method.Name) 852 | { 853 | case "HandleNonSuccessAndDebuggerNotification": 854 | case "ThrowForNonSuccess": 855 | case "ValidateEnd": 856 | case "GetResult": 857 | return false; 858 | } 859 | } 860 | else if (type.FullName == "System.ThrowHelper") 861 | { 862 | return false; 863 | } 864 | 865 | return true; 866 | } 867 | 868 | private static bool IsStackTraceHidden(MemberInfo memberInfo) 869 | { 870 | if (StackTraceHiddenAttributeType is not null && !memberInfo.Module.Assembly.ReflectionOnly) 871 | { 872 | return memberInfo.GetCustomAttributes(StackTraceHiddenAttributeType, false).Length != 0; 873 | } 874 | 875 | EnumerableIList attributes; 876 | try 877 | { 878 | attributes = EnumerableIList.Create(memberInfo.GetCustomAttributesData()); 879 | } 880 | catch (NotImplementedException) 881 | { 882 | return false; 883 | } 884 | 885 | foreach (var attribute in attributes) 886 | { 887 | // reflection-only attribute, match on name 888 | if (attribute.AttributeType.FullName == StackTraceHiddenAttributeType?.FullName) 889 | { 890 | return true; 891 | } 892 | } 893 | 894 | return false; 895 | } 896 | 897 | #if NET6_0_OR_GREATER 898 | [RequiresUnreferencedCode(Constants.TrimWarning)] 899 | #endif 900 | // https://github.com/dotnet/runtime/blob/c985bdcec2a9190e733bcada413a193d5ff60c0d/src/libraries/System.Private.CoreLib/src/System/Diagnostics/StackTrace.cs#L375-L430 901 | private static bool TryResolveStateMachineMethod(ref MethodBase method, out Type declaringType) 902 | { 903 | if (method.DeclaringType is null) 904 | { 905 | declaringType = null!; 906 | return false; 907 | } 908 | declaringType = method.DeclaringType; 909 | 910 | var parentType = declaringType.DeclaringType; 911 | if (parentType is null) 912 | { 913 | return false; 914 | } 915 | 916 | static MethodInfo[] GetDeclaredMethods(Type type) => 917 | type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); 918 | 919 | var methods = GetDeclaredMethods(parentType); 920 | if (methods == null) 921 | { 922 | return false; 923 | } 924 | 925 | foreach (var candidateMethod in methods) 926 | { 927 | var attributes = candidateMethod.GetCustomAttributes(inherit: false); 928 | // ReSharper disable once ConditionIsAlwaysTrueOrFalse - Taken from CoreFX 929 | if (attributes is null) 930 | { 931 | continue; 932 | } 933 | 934 | bool foundAttribute = false, foundIteratorAttribute = false; 935 | foreach (var asma in attributes) 936 | { 937 | if (asma.StateMachineType == declaringType) 938 | { 939 | foundAttribute = true; 940 | foundIteratorAttribute |= asma is IteratorStateMachineAttribute 941 | || AsyncIteratorStateMachineAttributeType != null 942 | && AsyncIteratorStateMachineAttributeType.IsInstanceOfType(asma); 943 | } 944 | } 945 | 946 | if (foundAttribute) 947 | { 948 | // If this is an iterator (sync or async), mark the iterator as changed, so it gets the + annotation 949 | // of the original method. Non-iterator async state machines resolve directly to their builder methods 950 | // so aren't marked as changed. 951 | method = candidateMethod; 952 | declaringType = candidateMethod.DeclaringType!; 953 | return foundIteratorAttribute; 954 | } 955 | } 956 | return false; 957 | } 958 | 959 | internal enum GeneratedNameKind 960 | { 961 | None = 0, 962 | 963 | // Used by EE: 964 | ThisProxyField = '4', 965 | HoistedLocalField = '5', 966 | DisplayClassLocalOrField = '8', 967 | LambdaMethod = 'b', 968 | LambdaDisplayClass = 'c', 969 | StateMachineType = 'd', 970 | LocalFunction = 'g', // note collision with Deprecated_InitializerLocal, however this one is only used for method names 971 | 972 | // Used by EnC: 973 | AwaiterField = 'u', 974 | HoistedSynthesizedLocalField = 's', 975 | 976 | // Currently not parsed: 977 | StateMachineStateField = '1', 978 | IteratorCurrentBackingField = '2', 979 | StateMachineParameterProxyField = '3', 980 | ReusableHoistedLocalField = '7', 981 | LambdaCacheField = '9', 982 | FixedBufferField = 'e', 983 | AnonymousType = 'f', 984 | TransparentIdentifier = 'h', 985 | AnonymousTypeField = 'i', 986 | AutoPropertyBackingField = 'k', 987 | IteratorCurrentThreadIdField = 'l', 988 | IteratorFinallyMethod = 'm', 989 | BaseMethodWrapper = 'n', 990 | AsyncBuilderField = 't', 991 | DynamicCallSiteContainerType = 'o', 992 | DynamicCallSiteField = 'p' 993 | } 994 | } 995 | } 996 | --------------------------------------------------------------------------------