├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── ScriptBlockDisassembler.sln └── src └── ScriptBlockDisassembler ├── AstExtensions.cs ├── DisassemblerOptions.cs ├── DynamicStringBuilder.cs ├── DynamicTranslation.cs ├── Ensure.cs ├── Flags.cs ├── FormatExpressionTree.cs ├── GetScriptBlockDisassemblyCommand.cs ├── PSExpressionTranslation.cs ├── PSTranslationSettings.cs ├── RecursiveReducer.cs ├── ReflectionExtensions.cs ├── ScriptBlockDisassembler.csproj ├── ScriptBlockDisassembler.psd1 ├── Throw.cs └── TypeIsTranslation.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | env: 12 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 13 | POWERSHELL_TELEMETRY_OPTOUT: 1 14 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 15 | DOTNET_NOLOGO: true 16 | 17 | defaults: 18 | run: 19 | shell: pwsh 20 | 21 | jobs: 22 | build: 23 | name: Build 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | os: [ ubuntu-latest, macos-latest, windows-latest ] 29 | steps: 30 | - uses: actions/checkout@v1 31 | - uses: actions/setup-dotnet@v1 32 | with: 33 | dotnet-version: '6.0.x' 34 | - name: Build 35 | run: dotnet publish --configuration Release 36 | - uses: actions/upload-artifact@v1 37 | if: matrix.os == 'windows-latest' 38 | with: 39 | name: ScriptBlockDisassembler 40 | path: ./src/ScriptBlockDisassembler/bin/Release/net6.0/publish 41 | - uses: actions/upload-artifact@v1 42 | if: matrix.os != 'windows-latest' 43 | with: 44 | name: ScriptBlockDisassembler-${{ matrix.os }} 45 | path: ./src/ScriptBlockDisassembler/bin/Release/net6.0/publish 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Attach", 9 | "type": "coreclr", 10 | "request": "attach", 11 | }, 12 | { 13 | "name": ".NET Core Attach non-JMC", 14 | "type": "coreclr", 15 | "request": "attach", 16 | "justMyCode": false, 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Patrick Meinecke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ScriptBlockDisassembler

2 | 3 |

4 | 5 | Show a pseudo-C# representation of the code that the PowerShell compiler generates for a given ScriptBlock. 6 | 7 |

8 | 9 | Build Status 10 | 11 | 12 | PowerShell Gallery Version (including pre-releases) 13 | 14 | 15 | GitHub 16 | 17 |

18 | 19 | ## Install 20 | 21 | ```powershell 22 | Install-Module ScriptBlockDisassembler -Scope CurrentUser -Force 23 | ``` 24 | 25 | ## Why 26 | 27 | Ever try to read [`Compiler.cs`][compiler] in [PowerShell/PowerShell][powershell]? It's doable, but tedious. Especially for more complex issues it'd be nice to just get a readable version of the final expression tree. 28 | 29 | So I wrote this. It forces the `ScriptBlock` to be compiled, and then digs into it with reflection to find the LINQ expression tree it generated. Then runs it through [ReadableExpressions][readable] with some PowerShell specific customizations and boom we got something much easier to understand. 30 | 31 | You may want this if: 32 | 33 | 1. You're working on the compiler 34 | 2. You're just curious how certain PowerShell code is compiled 35 | 36 | ## Demo 37 | 38 | ```powershell 39 | { $a = 10 } | Get-ScriptBlockDisassembly 40 | ``` 41 | 42 | ```csharp 43 | // ScriptBlock.EndBlock 44 | (FunctionContext funcContext) => 45 | { 46 | ExecutionContext context; 47 | try 48 | { 49 | context = funcContext._executionContext; 50 | MutableTuple locals = (MutableTuple)funcContext._localsTuple; 51 | funcContext._functionName = ""; 52 | funcContext._currentSequencePointIndex = 0; 53 | context._debugger.EnterScriptFunction(funcContext); 54 | try 55 | { 56 | funcContext._currentSequencePointIndex = 1; 57 | 58 | if (context._debuggingMode > 0) 59 | { 60 | context._debugger.OnSequencePointHit(funcContext); 61 | } 62 | 63 | // Note, this here is the actual $a = 10 64 | locals.Item009 = 10; 65 | context.QuestionMarkVariableValue = true; 66 | } 67 | catch (FlowControlException) 68 | { 69 | throw; 70 | } 71 | catch (Exception exception) 72 | { 73 | ExceptionHandlingOps.CheckActionPreference(funcContext, exception); 74 | } 75 | funcContext._currentSequencePointIndex = 2; 76 | 77 | if (context._debuggingMode > 0) 78 | { 79 | context._debugger.OnSequencePointHit(funcContext); 80 | } 81 | 82 | } 83 | finally 84 | { 85 | context._debugger.ExitScriptFunction(); 86 | } 87 | } 88 | ``` 89 | 90 | ## How do I read it? 91 | 92 | Each named block has it's own delegate (of type `Action`) that will be disassembled. So at minimum 93 | each block will look like this: 94 | 95 | ```csharp 96 | // ScriptBlock.EndBlock 97 | (FunctionContext context) => 98 | { 99 | } 100 | ``` 101 | 102 | Any time you see a static method called from the class `Fake`, that is a representation of something 103 | not directly translatable to C#. 104 | 105 | ### Fake.Dynamic 106 | 107 | Any time you see `Fake.Dynamic` you should imagine it as having this signature: 108 | 109 | ```csharp 110 | class Fake 111 | { 112 | public static TDelegate Dynamic(DynamicMetaObjectBinder binder); 113 | } 114 | ``` 115 | 116 | So a call to `Fake.Dynamic>(PSPipeWriterBinder.Get())(10)` would get a `Action` from 117 | `Fake.Dynamic` using the `PSPipeWriterBinder` call site binder and then invoke it. 118 | 119 | In reality the call looks roughly more like: 120 | 121 | ```csharp 122 | closure.Constants[0].Target.Invoke(closure.Constants[0], closure.Constants[1], 10); 123 | ``` 124 | 125 | But that doesn't really tell you anything helpful without examining the constants directly. 126 | 127 | ### Fake.Const 128 | 129 | This represents the retrieval of a constant that is embedded in the delegate. 130 | 131 | ```csharp 132 | Fake.Const(typeof(CommandExpressionAst), "10"); 133 | // | | | 134 | // | | -- ToString value 135 | // | -- Optional runtime type if different than static type 136 | // -- What the constant is statically typed as 137 | ``` 138 | 139 | ## What about binders? How can I read those? 140 | 141 | I've added the `Format-ExpressionTree` command to help with that. You're currently 142 | on your own as far as obtaining the expression (I recommend using [ImpliedReflection][ImpliedReflection]), 143 | but once you've obtained one you can pipe it to that command. 144 | 145 | ## Should I use this in a production environment? 146 | 147 | No. I don't know why you would, but don't. It relies heavily on implementation detail 148 | and will certainly break eventually. Maybe even with a minor release. 149 | 150 | This module should only really ever be used interactively for troubleshooting or exploration. 151 | 152 | The code is also far from optimal in a lot of places. Please don't use it as an example of what you should do. 153 | 154 | ## Can I compile the C#? 155 | 156 | No. The LINQ expression tree that PowerShell generates makes *heavy* use of constant 157 | expressions that cannot easily be translated to pure source code. Any time you see 158 | a method call on a class called `Fake`, that's just some psuedo code I put in to 159 | express what is happening. 160 | 161 | It also makes heavy use of dynamic expressions. For these I use the state of the passed 162 | binder to recreate an approximation of it's construction. 163 | 164 | Also most of the API's called in the disassemblied result are non-public. 165 | 166 | Also LINQ expression trees let you do things like fit a whole block of statements into a single expression. 167 | 168 | ## Optimized vs unoptimized 169 | 170 | There are two modes for the compiler, optimized and unoptimized. By default this command will return the optimized version, but the `-Unoptimized` switch can be specified to change that. 171 | 172 | Here are some common reasons the compiler will naturally enter the unoptimized mode: 173 | 174 | 1. Dot sourcing 175 | 2. Static analysis found the use of a `*-Variable` command 176 | 3. Static analysis found the use of *any* debugger command 177 | 4. Static analysis found references to any `AllScope` variables 178 | 179 | Optimization mostly affects how access of local variables are generated. 180 | 181 | ## Why doesn't this work on PowerShell versions older than 7.2 182 | 183 | I just didn't see the need and it would require me to make sure all the private fields for every binder are still the same. If you need this please open an issue. 184 | 185 | [readable]: https://github.com/agileobjects/ReadableExpressions "agileobjects/ReadableExpressions" 186 | [compiler]: https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/parser/Compiler.cs "Compiler.cs" 187 | [powershell]: https://github.com/PowerShell/PowerShell "PowerShell/PowerShell" 188 | [ImpliedReflection]: https://github.com/SeeminglyScience/ImpliedReflection "SeeminglyScience/ImpliedReflection" 189 | -------------------------------------------------------------------------------- /ScriptBlockDisassembler.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A81A674B-978D-469D-8F9D-5617ECEC4187}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScriptBlockDisassembler", "src\ScriptBlockDisassembler\ScriptBlockDisassembler.csproj", "{0463F6E0-DB72-480A-8604-01EA4701CAB7}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {0463F6E0-DB72-480A-8604-01EA4701CAB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {0463F6E0-DB72-480A-8604-01EA4701CAB7}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {0463F6E0-DB72-480A-8604-01EA4701CAB7}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {0463F6E0-DB72-480A-8604-01EA4701CAB7}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(NestedProjects) = preSolution 25 | {0463F6E0-DB72-480A-8604-01EA4701CAB7} = {A81A674B-978D-469D-8F9D-5617ECEC4187} 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/AstExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation.Language; 3 | using System.Reflection; 4 | 5 | namespace ScriptBlockDisassembler 6 | { 7 | internal static class AstExtensions 8 | { 9 | private static readonly Lazy> s_getCleanBlock 10 | = new(() => 11 | { 12 | return typeof(ScriptBlockAst).GetProperty( 13 | "CleanBlock", 14 | BindingFlags.Instance | BindingFlags.Public) 15 | ?.GetGetMethod() 16 | ?.CreateDelegate>() 17 | ?? new Func(_ => null); 18 | }); 19 | 20 | public static NamedBlockAst? GetCleanBlock(this ScriptBlockAst scriptBlockAst) 21 | => s_getCleanBlock.Value(scriptBlockAst); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/DisassemblerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace ScriptBlockDisassembler; 2 | 3 | internal sealed record DisassemblerOptions( 4 | bool IgnoreUpdatePosition, 5 | bool Unoptimized, 6 | bool IgnoreStartupAndTeardown, 7 | bool IgnoreQuestionMarkVariable) 8 | { 9 | public static DisassemblerOptions Default { get; } = new( 10 | IgnoreUpdatePosition: false, 11 | Unoptimized: false, 12 | IgnoreStartupAndTeardown: false, 13 | IgnoreQuestionMarkVariable: false); 14 | } 15 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/DynamicStringBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Dynamic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Management.Automation.Language; 6 | using System.Runtime.CompilerServices; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | 10 | namespace ScriptBlockDisassembler; 11 | 12 | internal class DynamicStringBuilder 13 | { 14 | private readonly StringBuilder _sb; 15 | 16 | private readonly DynamicExpression _expression; 17 | 18 | public DynamicStringBuilder(DynamicExpression expression) 19 | { 20 | _sb = new(); 21 | _expression = expression; 22 | } 23 | 24 | public DynamicStringBuilder Append(string value) 25 | { 26 | _sb.Append(value); 27 | return this; 28 | } 29 | 30 | private DynamicStringBuilder Append(object value) 31 | { 32 | _sb.Append(value); 33 | return this; 34 | } 35 | 36 | private DynamicStringBuilder Append(char value) 37 | { 38 | _sb.Append(value); 39 | return this; 40 | } 41 | 42 | private DynamicStringBuilder AppendTypeExpression(Type? type, string argName = "type") 43 | { 44 | if (type is null) 45 | { 46 | return Append(argName).Append(": null"); 47 | } 48 | 49 | return Append("typeof(").AppendTypeName(type).Append(')'); 50 | } 51 | 52 | internal DynamicStringBuilder AppendTypeName(Type type) 53 | { 54 | if (type == typeof(void)) return Append("void"); 55 | if (type == typeof(short)) return Append("short"); 56 | if (type == typeof(byte)) return Append("byte"); 57 | if (type == typeof(int)) return Append("int"); 58 | if (type == typeof(long)) return Append("long"); 59 | if (type == typeof(sbyte)) return Append("sbyte"); 60 | if (type == typeof(ushort)) return Append("ushort"); 61 | if (type == typeof(uint)) return Append("uint"); 62 | if (type == typeof(ulong)) return Append("ulong"); 63 | if (type == typeof(float)) return Append("float"); 64 | if (type == typeof(double)) return Append("double"); 65 | if (type == typeof(decimal)) return Append("decimal"); 66 | if (type == typeof(object)) return Append("object"); 67 | if (type == typeof(char)) return Append("char"); 68 | if (type == typeof(string)) return Append("string"); 69 | if (type == typeof(nint)) return Append("nint"); 70 | if (type == typeof(nuint)) return Append("nuint"); 71 | if (type == typeof(bool)) return Append("bool"); 72 | 73 | if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) 74 | { 75 | return AppendTypeName(type.GetElementType()!).Append('?'); 76 | } 77 | 78 | if (type.IsPointer) 79 | { 80 | return AppendTypeName(type.GetElementType()!).Append('*'); 81 | } 82 | 83 | if (type.IsByRef) 84 | { 85 | return Append("ref ").AppendTypeName(type.GetElementType()!); 86 | } 87 | 88 | if (type.IsArray) 89 | { 90 | if (type.IsSZArray) 91 | { 92 | return AppendTypeName(type.GetElementType()!).Append("[]"); 93 | } 94 | 95 | int rank = type.GetArrayRank(); 96 | return AppendTypeName(type.GetElementType()!). 97 | Append('[').Append(new string(',', rank - 1)).Append(']'); 98 | } 99 | 100 | if (!type.IsGenericType) 101 | { 102 | return Append(type.Name); 103 | } 104 | 105 | Type[] genericTypes = type.GetGenericArguments(); 106 | Append(Regex.Replace(type.Name, @"`\d+$", string.Empty)).Append('<'); 107 | if (type.IsConstructedGenericType) 108 | { 109 | AppendTypeName(genericTypes[0]); 110 | for (int i = 1; i < genericTypes.Length; i++) 111 | { 112 | Append(", ").AppendTypeName(genericTypes[i]); 113 | } 114 | 115 | return Append('>'); 116 | } 117 | 118 | if (genericTypes.Length is 1) 119 | { 120 | return Append('>'); 121 | } 122 | 123 | return Append(new string(',', genericTypes.Length - 1)).Append('>'); 124 | } 125 | 126 | public DynamicStringBuilder AppendDynamicExpression() 127 | { 128 | Append("Fake.Dynamic<").AppendTypeName(_expression.DelegateType).Append(">("); 129 | return AppendBinder(_expression.Binder); 130 | } 131 | 132 | public override string ToString() => _sb.ToString(); 133 | 134 | private DynamicStringBuilder AppendCallInfo(CallInfo? callInfo, string argName = "callInfo") 135 | { 136 | if (callInfo is null) 137 | { 138 | return Append(argName).Append(": null"); 139 | } 140 | 141 | // Technically this ctor has two args but PowerShell never uses the 142 | // other so it's just clutter. 143 | return Append("new CallInfo(").Append(callInfo.ArgumentCount.ToString()).Append(")"); 144 | } 145 | 146 | private DynamicStringBuilder AppendString(string value, string argName = "name") 147 | { 148 | if (value is null) 149 | { 150 | return Append(argName).Append(": null"); 151 | } 152 | 153 | return Append('"').Append(value).Append('"'); 154 | } 155 | 156 | private DynamicStringBuilder AppendTypeDefinitionAst(Type? type, string argName = "classScopeAst") 157 | { 158 | if (type is null) 159 | { 160 | return Append(argName).Append(": null"); 161 | } 162 | 163 | return Append("Fake.Const(name: \"").Append(type.Name).Append("\")"); 164 | } 165 | 166 | private DynamicStringBuilder AppendInvocationConstraints(object? constraints) 167 | { 168 | if (constraints is null) 169 | { 170 | return Append("constraints: null"); 171 | } 172 | 173 | Type? methodTargetType = constraints.AccessProperty("MethodTargetType"); 174 | Type[] parameterTypes = constraints.AccessProperty("ParameterTypes")!; 175 | if (methodTargetType is null && (parameterTypes is null || parameterTypes.All(t => t is null))) 176 | { 177 | // This isn't a thing, but the constructor for the average case is so noisy. 178 | return Append("PSMethodInvocationConstraints.Default"); 179 | } 180 | 181 | Append("new PSMethodInvocationConstraints("); 182 | if (methodTargetType is not null) 183 | { 184 | AppendTypeExpression(methodTargetType); 185 | } 186 | else 187 | { 188 | Append("methodTargetType: null"); 189 | } 190 | 191 | if (parameterTypes is null) 192 | { 193 | return Append(", parameterTypes: null)"); 194 | } 195 | 196 | Append(", new Type[] { "); 197 | if (parameterTypes.Length is not 0) 198 | { 199 | AppendTypeExpression(parameterTypes[0]); 200 | for (int i = 1; i < parameterTypes.Length; i++) 201 | { 202 | Append(", ").AppendTypeExpression(parameterTypes[i]); 203 | } 204 | } 205 | 206 | return Append(" })"); 207 | } 208 | 209 | internal DynamicStringBuilder AppendEnum(object enumValue) 210 | { 211 | return AppendEnum(enumValue.GetType().Name, enumValue); 212 | } 213 | 214 | internal DynamicStringBuilder AppendEnum(string enumName, object enumValue) 215 | { 216 | string value = enumValue.ToString()!; 217 | if (value.Contains(',')) 218 | { 219 | return Append(enumName).Append(".") 220 | .Append(string.Join($" | {enumName}.", Regex.Split(value, ", ?"))); 221 | } 222 | 223 | if (Regex.IsMatch(value, @"^\d+$")) 224 | { 225 | return Append('(').Append(enumName).Append(')').Append(value); 226 | } 227 | 228 | return Append(enumName).Append('.').Append(value); 229 | } 230 | 231 | private DynamicStringBuilder AppendBool(string argName, bool value) 232 | { 233 | Append(argName).Append(": "); 234 | if (value) 235 | { 236 | return Append("true"); 237 | } 238 | 239 | return Append("false"); 240 | } 241 | 242 | private DynamicStringBuilder AppendBinder(CallSiteBinder binder) 243 | { 244 | var binderName = binder.GetType().Name; 245 | bool isNoArgsGetMethods = binderName is "PSEnumerableBinder" 246 | or "PSToObjectArrayBinder" 247 | or "PSPipeWriterBinder" 248 | or "PSToStringBinder" 249 | or "PSPipelineResultToBoolBinder" 250 | or "PSCustomObjectConverter" 251 | or "PSDynamicConvertBinder" 252 | or "PSVariableAssignmentBinder"; 253 | 254 | if (isNoArgsGetMethods) 255 | { 256 | return Append(binderName).Append(".Get()"); 257 | } 258 | 259 | if (binderName is "ReservedMemberBinder") 260 | { 261 | return Append("new PSGetMemberBinder.ReservedMemberBinder(") 262 | .AppendString(binder.AccessProperty("Name") ?? "").Append(", ") 263 | .AppendBool("ignoreCase", binder.AccessProperty("IgnoreCase")).Append(", ") 264 | .Append("static: false)"); 265 | } 266 | 267 | if (binderName is "PSArrayAssignmentRHSBinder") 268 | { 269 | return Append(binderName).Append(".Get(").Append(binder.AccessField("_elements")).Append(')'); 270 | } 271 | 272 | if (binderName is "PSInvokeDynamicMemberBinder") 273 | { 274 | return Append(binderName).Append(".Get(") 275 | .AppendCallInfo(binder.AccessField("_callInfo")).Append(", ") 276 | .AppendTypeDefinitionAst(binder.AccessField("_classScope")).Append(", ") 277 | .AppendBool("static", binder.AccessField("_static")).Append(", ") 278 | .AppendBool("propertySetter", binder.AccessField("_propertySetter")).Append(", ") 279 | .AppendInvocationConstraints(binder.AccessField("_constraints")) 280 | .Append(')'); 281 | } 282 | 283 | if (binderName is "PSGetDynamicMemberBinder" or "PSSetDynamicMemberBinder") 284 | { 285 | return Append(binderName).Append(".Get(") 286 | .AppendTypeDefinitionAst(binder.AccessField("_classScope")).Append(", ") 287 | .AppendBool("static", binder.AccessField("_static")).Append(')'); 288 | } 289 | 290 | if (binderName is "PSSwitchClauseEvalBinder") 291 | { 292 | return Append(binderName).Append(".Get(") 293 | .AppendEnum("SwitchFlags", binder.AccessField("_flags")).Append(")"); 294 | } 295 | 296 | if (binderName is "PSInvokeBinder" or "ComInvokeAction") 297 | { 298 | return Append($"new {binderName}(").AppendCallInfo(binder.AccessProperty("CallInfo")).Append(")"); 299 | } 300 | 301 | if (binderName is "SplatInvokeBinder") 302 | { 303 | return Append($"{binderName}.Instance"); 304 | } 305 | 306 | if (binderName is "PSAttributeGenerator") 307 | { 308 | return Append(binderName).Append(".Get(") 309 | .AppendCallInfo(binder.AccessProperty("CallInfo")).Append(')'); 310 | } 311 | 312 | if (binderName is "PSBinaryOperationBinder") 313 | { 314 | return Append(binderName).Append(".Get(") 315 | .AppendEnum("ExpressionType", binder.AccessProperty("Operation")).Append(", ") 316 | .AppendBool("ignoreCase", binder.AccessField("_ignoreCase")).Append(',') 317 | .AppendBool("scalarCompare", binder.AccessField("_scalarCompare")).Append(')'); 318 | } 319 | 320 | if (binderName is "PSUnaryOperationBinder") 321 | { 322 | return Append(binderName).Append(".Get(") 323 | .AppendEnum("ExpressionType", binder.AccessProperty("Operation")).Append(")"); 324 | } 325 | 326 | if (binderName is "PSConvertBinder") 327 | { 328 | return Append(binderName).Append(".Get(") 329 | .AppendTypeExpression(binder.AccessProperty("Type")).Append(')'); 330 | } 331 | 332 | if (binderName is "PSGetIndexBinder") 333 | { 334 | return Append(binderName).Append(".Get(") 335 | .Append("argCount: ").Append(binder.AccessProperty("CallInfo")?.ArgumentCount ?? 0).Append(", ") 336 | .AppendInvocationConstraints(binder.AccessField("_constraints")).Append(", ") 337 | .AppendBool("allowSlicing", binder.AccessField("_allowSlicing")).Append(')'); 338 | } 339 | 340 | if (binderName is "PSSetIndexBinder") 341 | { 342 | return Append(binderName).Append(".Get(") 343 | .Append("argCount: ").Append(binder.AccessProperty("CallInfo")?.ArgumentCount ?? 0).Append(", ") 344 | .AppendInvocationConstraints(binder.AccessField("_constraints")).Append(')'); 345 | } 346 | 347 | if (binderName is "PSGetMemberBinder") 348 | { 349 | return Append(binderName).Append(".Get(") 350 | .AppendString(binder.AccessProperty("Name") ?? "").Append(", ") 351 | .AppendTypeExpression(binder.AccessField("_classScope")).Append(", ") 352 | .AppendBool("static", binder.AccessField("_static")).Append(", ") 353 | .AppendBool("nonEnumerating", binder.AccessField("_nonEnumerating")).Append(')'); 354 | } 355 | 356 | if (binderName is "PSSetMemberBinder") 357 | { 358 | return Append(binderName).Append(".Get(") 359 | .AppendString(binder.AccessProperty("Name") ?? "").Append(", ") 360 | .AppendTypeExpression(binder.AccessField("_classScope")).Append(", ") 361 | .AppendBool("static", binder.AccessField("_static")).Append(')'); 362 | } 363 | 364 | if (binderName is "PSInvokeMemberBinder") 365 | { 366 | return Append(binderName).Append(".Get(") 367 | .AppendString(binder.AccessProperty("Name") ?? "").Append(", ") 368 | .AppendTypeExpression(binder.AccessField("_classScope"), "classScope").Append(", ") 369 | .AppendCallInfo(binder.AccessProperty("CallInfo")).Append(", ") 370 | .AppendBool("static", binder.AccessField("_static")).Append(", ") 371 | .AppendBool("nonEnumerating", binder.AccessField("_nonEnumerating")).Append(", ") 372 | .AppendInvocationConstraints(binder.AccessField("_invocationConstraints")).Append(')'); 373 | } 374 | 375 | if (binderName is "PSCreateInstanceBinder") 376 | { 377 | return Append(binderName).Append(".Get(") 378 | .AppendCallInfo(binder.AccessProperty("CallInfo")).Append(", ") 379 | .AppendInvocationConstraints(binder.AccessField("_constraints")).Append(", ") 380 | .AppendBool("publicTypeOnly", binder.AccessField("_publicTypeOnly")).Append(')'); 381 | } 382 | 383 | if (binderName is "PSInvokeBaseCtorBinder") 384 | { 385 | return Append(binderName).Append(".Get(") 386 | .AppendCallInfo(binder.AccessProperty("CallInfo")).Append(", ") 387 | .AppendInvocationConstraints(binder.AccessField("_constraints")).Append(')'); 388 | } 389 | 390 | return Append("Fake.UnhandledBinder<").Append(binderName).Append(">()"); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/DynamicTranslation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Text.RegularExpressions; 5 | using AgileObjects.ReadableExpressions.Translations; 6 | 7 | namespace ScriptBlockDisassembler; 8 | 9 | internal sealed class DynamicTranslation : ITranslation 10 | { 11 | private readonly DynamicExpression _expression; 12 | 13 | private readonly string _value; 14 | 15 | private readonly ITranslation[] _arguments; 16 | 17 | private readonly Lazy _argumentsLineCount; 18 | 19 | private DynamicTranslation(string value, DynamicExpression expression, ITranslation[] arguments) 20 | { 21 | _value = value; 22 | _expression = expression; 23 | _arguments = arguments; 24 | _argumentsLineCount = new(() => 25 | { 26 | int total = 0; 27 | foreach (ITranslation argument in _arguments) 28 | { 29 | int current = argument.GetLineCount(); 30 | if (argument.NodeType is ExpressionType.Block) 31 | { 32 | current += 2; 33 | } 34 | 35 | total += current; 36 | } 37 | 38 | if (total == _arguments.Length && _arguments.Length < 3) 39 | { 40 | return 0; 41 | } 42 | 43 | return total; 44 | }); 45 | } 46 | 47 | public static ITranslation GetTranslationFor(Expression expression, ExpressionTranslation translator) 48 | { 49 | if (expression is not DynamicExpression dynamicExpression) 50 | { 51 | return translator.GetTranslationFor(expression); 52 | } 53 | 54 | ITranslation[] arguments = new ITranslation[dynamicExpression.Arguments.Count]; 55 | for (int i = 0; i < arguments.Length; i++) 56 | { 57 | arguments[i] = translator.GetTranslationFor(dynamicExpression.Arguments[i]); 58 | } 59 | 60 | DynamicStringBuilder sb = new(dynamicExpression); 61 | return new DynamicTranslation( 62 | sb.AppendDynamicExpression().ToString(), 63 | dynamicExpression, 64 | arguments); 65 | } 66 | 67 | public ExpressionType NodeType => ExpressionType.Dynamic; 68 | 69 | public Type Type => _expression.Type; 70 | 71 | public int TranslationSize => _value.Length 72 | + (_arguments.Length * 2) 73 | + _arguments.Sum(arg => arg.TranslationSize) 74 | + 1; 75 | 76 | public int FormattingSize => 0; 77 | 78 | public int GetIndentSize() => 0; 79 | 80 | public int GetLineCount() => Regex.Matches(_value, @"\r?\n").Count + _argumentsLineCount.Value; 81 | 82 | public void WriteTo(TranslationWriter writer) 83 | { 84 | writer.WriteToTranslation(_value); 85 | if (_arguments.Length is 0) 86 | { 87 | writer.WriteToTranslation(")()"); 88 | return; 89 | } 90 | 91 | writer.WriteToTranslation(")("); 92 | if (_argumentsLineCount.Value is not 0) 93 | { 94 | writer.WriteNewLineToTranslation(); 95 | writer.Indent(); 96 | } 97 | 98 | WriteArgument(_arguments[0], writer); 99 | for (int i = 1; i < _arguments.Length; i++) 100 | { 101 | writer.WriteToTranslation(","); 102 | if (_argumentsLineCount.Value is 0) 103 | { 104 | writer.WriteToTranslation(" "); 105 | } 106 | else 107 | { 108 | writer.WriteNewLineToTranslation(); 109 | } 110 | 111 | WriteArgument(_arguments[i], writer); 112 | } 113 | 114 | writer.WriteToTranslation(")"); 115 | if (_argumentsLineCount.Value is not 0) 116 | { 117 | writer.Unindent(); 118 | } 119 | 120 | static void WriteArgument(ITranslation translation, TranslationWriter writer) 121 | { 122 | if (translation.NodeType is ExpressionType.Block) 123 | { 124 | writer.WriteToTranslation("{"); 125 | writer.WriteNewLineToTranslation(); 126 | writer.Indent(); 127 | } 128 | 129 | translation.WriteTo(writer); 130 | if (translation.NodeType is ExpressionType.Block) 131 | { 132 | writer.WriteNewLineToTranslation(); 133 | writer.Unindent(); 134 | writer.WriteToTranslation("}"); 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/Ensure.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace ScriptBlockDisassembler; 4 | 5 | internal static class Ensure 6 | { 7 | public static void UnsupportedNotNull([NotNull] object? value, string fieldName) 8 | { 9 | if (value is null) 10 | { 11 | Throw.SomethingChanged($"{fieldName} is not null"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/Flags.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace ScriptBlockDisassembler; 4 | 5 | internal static class Flags 6 | { 7 | public static BindingFlags Get( 8 | bool isPublic = false, 9 | bool isPrivate = false, 10 | bool isStatic = false, 11 | bool isInstance = false) 12 | { 13 | return true switch 14 | { 15 | _ when isStatic => true switch 16 | { 17 | _ when isPublic => Static.Public, 18 | _ when isPrivate => Static.NonPublic, 19 | _ => Static.All, 20 | }, 21 | _ when isInstance => true switch 22 | { 23 | _ when isPublic => Instance.Public, 24 | _ when isPrivate => Instance.NonPublic, 25 | _ => Instance.All, 26 | }, 27 | _ => true switch 28 | { 29 | _ when isPublic => Public.All, 30 | _ when isPrivate => NonPublic.All, 31 | _ => All, 32 | }, 33 | }; 34 | } 35 | 36 | public const BindingFlags All = BindingFlags.Static 37 | | BindingFlags.Instance 38 | | BindingFlags.NonPublic 39 | | BindingFlags.Public; 40 | 41 | public static class Static 42 | { 43 | public const BindingFlags All = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; 44 | 45 | public const BindingFlags Public = BindingFlags.Static | BindingFlags.Public; 46 | 47 | public const BindingFlags NonPublic = BindingFlags.Static | BindingFlags.NonPublic; 48 | } 49 | 50 | public static class Instance 51 | { 52 | public const BindingFlags All = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; 53 | 54 | public const BindingFlags Public = BindingFlags.Instance | BindingFlags.Public; 55 | 56 | public const BindingFlags NonPublic = BindingFlags.Instance | BindingFlags.NonPublic; 57 | } 58 | 59 | public static class Public 60 | { 61 | public const BindingFlags All = BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; 62 | 63 | public const BindingFlags Static = BindingFlags.Public | BindingFlags.Static; 64 | 65 | public const BindingFlags Instance = BindingFlags.Public | BindingFlags.Instance; 66 | } 67 | 68 | public static class NonPublic 69 | { 70 | public const BindingFlags All = BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; 71 | 72 | public const BindingFlags Static = BindingFlags.NonPublic | BindingFlags.Static; 73 | 74 | public const BindingFlags Instance = BindingFlags.NonPublic | BindingFlags.Instance; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/FormatExpressionTree.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Linq.Expressions; 3 | using System.Management.Automation; 4 | 5 | namespace ScriptBlockDisassembler; 6 | 7 | [Cmdlet(VerbsCommon.Format, "ExpressionTree")] 8 | public sealed class FormatExpressionTree : PSCmdlet 9 | { 10 | [Parameter(Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] 11 | [ValidateNotNull] 12 | public Expression? Expression { get; set; } 13 | 14 | protected override void ProcessRecord() 15 | { 16 | Debug.Assert(Expression is not null); 17 | WriteObject( 18 | PSExpressionTranslation.Translate( 19 | Expression, 20 | DisassemblerOptions.Default)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/GetScriptBlockDisassemblyCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Management.Automation; 3 | using System.Management.Automation.Language; 4 | using System.Text; 5 | 6 | namespace ScriptBlockDisassembler 7 | { 8 | [Cmdlet(VerbsCommon.Get, "ScriptBlockDisassembly")] 9 | public sealed class GetScriptBlockDisassemblyCommand : PSCmdlet 10 | { 11 | private static readonly string[] s_defaultBlocks = 12 | { 13 | "clean", 14 | "dynamicparam", 15 | "begin", 16 | "process", 17 | "end", 18 | }; 19 | 20 | [Parameter(Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] 21 | [ValidateNotNull] 22 | public ScriptBlock ScriptBlock { get; set; } = null!; 23 | 24 | [Parameter(Position = 0)] 25 | [ValidateSet("begin", "process", "end", "dynamicparam", "clean")] 26 | public string[] Block { get; set; } = null!; 27 | 28 | [Parameter] 29 | public SwitchParameter Unoptimized { get; set; } 30 | 31 | [Parameter] 32 | public SwitchParameter Minimal { get; set; } 33 | 34 | [Parameter] 35 | public SwitchParameter IgnoreUpdatePosition { get; set; } 36 | 37 | [Parameter] 38 | public SwitchParameter IgnoreStartupAndTeardown { get; set; } 39 | 40 | [Parameter] 41 | public SwitchParameter IgnoreQuestionMarkVariable { get; set; } 42 | 43 | #if DEBUG 44 | public static Expression GetExpression( 45 | ScriptBlock scriptBlock, 46 | string blockName, 47 | bool unoptimized = false) 48 | { 49 | return PSExpressionTranslation.GetExpressionForScriptBlock( 50 | scriptBlock, 51 | blockName, 52 | DisassemblerOptions.Default with { Unoptimized = unoptimized }, 53 | out _); 54 | } 55 | #endif 56 | 57 | protected override void ProcessRecord() 58 | { 59 | bool omitErrors = false; 60 | if (Block is not { Length: > 0 }) 61 | { 62 | Block = s_defaultBlocks; 63 | omitErrors = true; 64 | } 65 | 66 | DisassemblerOptions options; 67 | if (Minimal) 68 | { 69 | options = DisassemblerOptions.Default with 70 | { 71 | Unoptimized = Unoptimized, 72 | IgnoreUpdatePosition = true, 73 | IgnoreStartupAndTeardown = true, 74 | IgnoreQuestionMarkVariable = true, 75 | }; 76 | } 77 | else 78 | { 79 | options = DisassemblerOptions.Default with 80 | { 81 | Unoptimized = Unoptimized, 82 | IgnoreUpdatePosition = IgnoreUpdatePosition, 83 | IgnoreStartupAndTeardown = IgnoreStartupAndTeardown, 84 | IgnoreQuestionMarkVariable = IgnoreQuestionMarkVariable, 85 | }; 86 | } 87 | 88 | StringBuilder text = new(); 89 | bool first = true; 90 | foreach (string block in Block) 91 | { 92 | string? result = ProcessBlock(block, omitErrors, options); 93 | if (result is null) 94 | { 95 | continue; 96 | } 97 | 98 | if (first) 99 | { 100 | first = false; 101 | } 102 | else 103 | { 104 | text.AppendLine().AppendLine(); 105 | } 106 | 107 | text.Append(result); 108 | } 109 | 110 | WriteObject(text.ToString()); 111 | } 112 | 113 | private string? ProcessBlock(string block, bool omitErrors, DisassemblerOptions options) 114 | { 115 | bool blockExists = block.ToLowerInvariant() switch 116 | { 117 | "begin" => GetBody(ScriptBlock).BeginBlock is not null, 118 | "process" => GetBody(ScriptBlock).ProcessBlock is not null, 119 | "end" => GetBody(ScriptBlock).EndBlock is not null, 120 | "dynamicparam" => GetBody(ScriptBlock).DynamicParamBlock is not null, 121 | "clean" => GetBody(ScriptBlock).GetCleanBlock() is not null, 122 | _ => Throw.Unreachable(), 123 | }; 124 | 125 | if (!blockExists) 126 | { 127 | if (!omitErrors) 128 | { 129 | WriteError( 130 | new ErrorRecord( 131 | new PSArgumentException( 132 | $"The specified named block '{block}' does not exist for this scriptblock.", 133 | nameof(block)), 134 | "BlockNotFound", 135 | ErrorCategory.InvalidArgument, 136 | ScriptBlock)); 137 | } 138 | 139 | return null; 140 | } 141 | 142 | return PSExpressionTranslation.Translate( 143 | ScriptBlock!, 144 | block, 145 | options); 146 | 147 | static ScriptBlockAst GetBody(ScriptBlock scriptBlock) 148 | { 149 | if (scriptBlock.Ast is FunctionDefinitionAst functionDefinitionAst) 150 | { 151 | return functionDefinitionAst.Body; 152 | } 153 | 154 | return (ScriptBlockAst)scriptBlock.Ast; 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/PSExpressionTranslation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Management.Automation; 4 | using System.Text.RegularExpressions; 5 | using AgileObjects.ReadableExpressions; 6 | using AgileObjects.ReadableExpressions.Translations; 7 | 8 | namespace ScriptBlockDisassembler; 9 | 10 | internal class PSExpressionTranslation : ExpressionTranslation 11 | { 12 | public PSExpressionTranslation(Expression expression, TranslationSettings settings) 13 | : base(expression, settings) 14 | { 15 | } 16 | 17 | protected PSExpressionTranslation(ExpressionAnalysis expressionAnalysis, TranslationSettings settings) 18 | : base(expressionAnalysis, settings) 19 | { 20 | } 21 | 22 | public static string Translate( 23 | ScriptBlock scriptBlock, 24 | string block, 25 | DisassemblerOptions options) 26 | { 27 | Expression? lambda = GetExpressionForScriptBlock( 28 | scriptBlock, 29 | block, 30 | options, 31 | out string propertyName); 32 | 33 | return string.Concat( 34 | $"// ScriptBlock.{propertyName}", 35 | Environment.NewLine, 36 | Translate(lambda, options)); 37 | } 38 | 39 | internal static Expression GetExpressionForScriptBlock( 40 | ScriptBlock scriptBlock, 41 | string block, 42 | DisassemblerOptions options, 43 | out string propertyName) 44 | { 45 | bool optimized = !options.Unoptimized; 46 | scriptBlock.InvokePrivateMethod( 47 | "Compile", 48 | new object[] { optimized }, 49 | new[] { typeof(bool) }); 50 | 51 | propertyName = block.ToLowerInvariant() switch 52 | { 53 | "begin" when optimized => "BeginBlock", 54 | "begin" when !optimized => "UnoptimizedBeginBlock", 55 | "process" when optimized => "ProcessBlock", 56 | "process" when !optimized => "UnoptimizedProcessBlock", 57 | "end" when optimized => "EndBlock", 58 | "end" when !optimized => "UnoptimizedEndBlock", 59 | "dynamicparam" when optimized => "DynamicParamBlock", 60 | "dynamicparam" when !optimized => "UnoptimizedDynamicParamBlock", 61 | "clean" when optimized => "CleanBlock", 62 | "clean" when !optimized => "UnoptimizedCleanBlock", 63 | _ => throw new ArgumentOutOfRangeException(nameof(block)), 64 | }; 65 | 66 | Delegate? action = scriptBlock.AccessProperty(propertyName); 67 | Ensure.UnsupportedNotNull(action, propertyName); 68 | Ensure.UnsupportedNotNull(action.Target, $"{propertyName}.Target"); 69 | 70 | object? delegateCreator = action.Target!.AccessField("_delegateCreator"); 71 | Ensure.UnsupportedNotNull(delegateCreator, "_delegateCreator"); 72 | 73 | Expression? lambda = delegateCreator.AccessField("_lambda"); 74 | Ensure.UnsupportedNotNull(lambda, "_lambda"); 75 | return lambda; 76 | } 77 | 78 | public static string Translate(Expression expression, DisassemblerOptions options) 79 | { 80 | Expression reduced = new RecursiveReduce(options).Visit(expression); 81 | PSExpressionTranslation translator = new( 82 | reduced, 83 | (TranslationSettings)PSTranslationSettings.Default); 84 | 85 | if (!options.IgnoreStartupAndTeardown) 86 | { 87 | return translator.GetTranslation(); 88 | } 89 | 90 | return Regex.Replace( 91 | translator.GetTranslation(), 92 | @"MutableTuple<.+?> locals;\r?\n", 93 | string.Empty); 94 | } 95 | 96 | public override ITranslation GetTranslationFor(Expression expression) 97 | { 98 | if (expression is DynamicExpression dynamic) 99 | { 100 | return DynamicTranslation.GetTranslationFor(dynamic, this); 101 | } 102 | 103 | if (expression is TypeBinaryExpression typeBinary) 104 | { 105 | return TypeIsTranslation.GetTranslationFor(typeBinary, this); 106 | } 107 | 108 | return base.GetTranslationFor(expression); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/PSTranslationSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using System.Management.Automation.Internal; 4 | using AgileObjects.ReadableExpressions; 5 | 6 | namespace ScriptBlockDisassembler; 7 | 8 | public class PSTranslationSettings : TranslationSettings 9 | { 10 | public static ITranslationSettings Default 11 | { 12 | get 13 | { 14 | var settings = (ITranslationSettings)new PSTranslationSettings(); 15 | return settings.ShowLambdaParameterTypes 16 | .ShowImplicitArrayTypes 17 | .ShowQuotedLambdaComments 18 | .UseExplicitTypeNames 19 | .UseExplicitGenericParameters 20 | .IndentUsing(" ") 21 | .TranslateConstantsUsing( 22 | (type, value) => ConstToString(type, value)); 23 | } 24 | } 25 | 26 | private static string ConstToString(Type type, object? value) 27 | { 28 | DynamicStringBuilder text = new(null!); 29 | return ConstToString(type, value, text).ToString(); 30 | } 31 | 32 | private static DynamicStringBuilder ConstToString(Type type, object? value, DynamicStringBuilder text) 33 | { 34 | if (value is null) 35 | { 36 | return text.Append("null"); 37 | } 38 | 39 | Type actualType = value.GetType(); 40 | if (value is PSObject pso && pso == AutomationNull.Value) 41 | { 42 | return text.Append("AutomationNull.Value"); 43 | } 44 | 45 | if (type == typeof(bool)) 46 | { 47 | return text.Append((bool)value ? "true" : "false"); 48 | } 49 | 50 | if (type == typeof(char)) 51 | { 52 | return text.Append($"'{value}'"); 53 | } 54 | 55 | if (type.IsPrimitive) 56 | { 57 | return text.Append(value.ToString() ?? Throw.Unreachable()); 58 | } 59 | 60 | if (type == typeof(string)) 61 | { 62 | if (((string)value).StartsWith("// Debug to")) 63 | { 64 | return text.Append((string)value); 65 | } 66 | 67 | return text.Append($"\"{value}\""); 68 | } 69 | 70 | if (actualType.IsEnum) 71 | { 72 | return text.AppendEnum(value); 73 | } 74 | 75 | text.Append("Fake.Const<") 76 | .AppendTypeName(type) 77 | .Append(">") 78 | .Append("("); 79 | 80 | if (value is Type staticType) 81 | { 82 | return text.Append("typeof(").AppendTypeName(staticType).Append("))"); 83 | } 84 | 85 | if (type.IsArray) 86 | { 87 | Type elementType = type.GetElementType()!; 88 | Array asArray = (Array)value; 89 | if (asArray.Length is 0) 90 | { 91 | return text.Append("/* empty */)"); 92 | } 93 | 94 | ConstToString(elementType, asArray.GetValue(0), text); 95 | for (int i = 1; i < asArray.Length; i++) 96 | { 97 | text.Append(", "); 98 | ConstToString(elementType, asArray.GetValue(i), text); 99 | } 100 | 101 | return text.Append(")"); 102 | } 103 | 104 | if (actualType != type) 105 | { 106 | text.Append("typeof(").AppendTypeName(value.GetType()).Append("), "); 107 | } 108 | 109 | if (actualType.Name.Equals("ScriptBlockExpressionWrapper")) 110 | { 111 | return text.Append($"\"{value.AccessField("_ast")}\")"); 112 | } 113 | 114 | string? toStringValue = value.ToString(); 115 | if (toStringValue is null or "") 116 | { 117 | return text.Append("\"_empty<").AppendTypeName(value.GetType()).Append(">\")"); 118 | } 119 | 120 | if (toStringValue.Equals(value.GetType().ToString(), StringComparison.Ordinal)) 121 | { 122 | return text.Append("\"_defaultToString<").AppendTypeName(value.GetType()).Append(">\")"); 123 | } 124 | 125 | return text.Append($"\"{value}\")"); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/RecursiveReducer.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using System.Collections.Concurrent; 5 | 6 | namespace ScriptBlockDisassembler; 7 | 8 | internal class RecursiveReduce : ExpressionVisitor 9 | { 10 | private readonly DisassemblerOptions _options; 11 | 12 | private int _untitledVariableId; 13 | 14 | private int _untitledLabelId; 15 | 16 | private readonly ConcurrentDictionary _map = new(); 17 | 18 | public RecursiveReduce(DisassemblerOptions options) => _options = options; 19 | 20 | public static Expression DefaultVisit(Expression node) 21 | { 22 | if (node is not Expression result) 23 | { 24 | return null!; 25 | } 26 | 27 | if (result.Reduce() is not Expression reduced) 28 | { 29 | return result; 30 | } 31 | 32 | return reduced; 33 | } 34 | 35 | private LabelTarget GetOrAddLabel(LabelTarget label) 36 | { 37 | return _map.GetOrAdd( 38 | label, 39 | _ => Expression.Label(label.Type, $"unnamed_label_{_untitledLabelId++}")); 40 | } 41 | 42 | protected override CatchBlock VisitCatchBlock(CatchBlock node) 43 | { 44 | if (node.Variable is { Name: null or "" }) 45 | { 46 | typeof(ParameterExpression) 47 | .GetField("k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)! 48 | .SetValue(node.Variable, $"unnamedVar{_untitledVariableId++}"); 49 | } 50 | 51 | return base.VisitCatchBlock(node); 52 | } 53 | 54 | protected override LabelTarget? VisitLabelTarget(LabelTarget? node) 55 | { 56 | if (node is null) 57 | { 58 | return null; 59 | } 60 | 61 | if (node.Name is null or "") 62 | { 63 | return GetOrAddLabel(node); 64 | } 65 | 66 | return node; 67 | } 68 | 69 | protected override Expression VisitDynamic(DynamicExpression node) 70 | { 71 | return node.Update(node.Arguments.Select(e => Visit(e))); 72 | } 73 | 74 | protected override Expression VisitExtension(Expression node) 75 | { 76 | if (node is DynamicExpression dynamicExpression) 77 | { 78 | return VisitDynamic(dynamicExpression); 79 | } 80 | 81 | if (_options.IgnoreUpdatePosition && node.GetType().Name is "UpdatePositionExpr") 82 | { 83 | return Expression.Empty(); 84 | } 85 | 86 | return base.Visit(node.Reduce()); 87 | } 88 | 89 | protected override ElementInit VisitElementInit(ElementInit node) 90 | => node.Update(node.Arguments.Select(e => Visit(e))); 91 | 92 | protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) 93 | { 94 | var result = Visit(node.Expression); 95 | if (result == node.Expression) 96 | { 97 | return node; 98 | } 99 | 100 | return node.Update(result); 101 | } 102 | 103 | protected override MemberListBinding VisitMemberListBinding(MemberListBinding node) 104 | => node.Update( 105 | node.Initializers.Select(init => init.Update(init.Arguments.Select(e => Visit(e))))); 106 | 107 | protected override SwitchCase VisitSwitchCase(SwitchCase node) 108 | { 109 | return node.Update( 110 | node.TestValues.Select(e => Visit(e)), 111 | Visit(node.Body)); 112 | } 113 | 114 | protected override Expression VisitGoto(GotoExpression node) 115 | { 116 | if (node.Target.Name is null or "") 117 | { 118 | node.Update( 119 | GetOrAddLabel(node.Target), 120 | base.Visit(node.Value)); 121 | } 122 | 123 | return base.VisitGoto((GotoExpression)node.Reduce()); 124 | } 125 | 126 | protected override Expression VisitBlock(BlockExpression node) 127 | { 128 | Expression[] children = node.Expressions 129 | .Select(child => Visit(child)) 130 | .Where(child => !(child is DefaultExpression defaultExpr && defaultExpr.Type == typeof(void))) 131 | .ToArray(); 132 | 133 | if (children is { Length: 0 }) 134 | { 135 | return node.Update( 136 | node.Variables.Select(child => Visit(child)).Cast(), 137 | node.Expressions.Select(child => Visit(child))); 138 | } 139 | 140 | if (children is { Length: 1 } && children[0].Type == node.Type) 141 | { 142 | return children[0]; 143 | } 144 | 145 | return node.Update( 146 | node.Variables.Select(child => Visit(child)).Cast(), 147 | children); 148 | } 149 | 150 | protected override Expression VisitLambda(Expression node) 151 | { 152 | if (!_options.IgnoreStartupAndTeardown) 153 | { 154 | return base.VisitLambda((Expression)node.Reduce()); 155 | } 156 | 157 | bool isInitialTryFinally = 158 | node.Body is BlockExpression { Expressions.Count: 1 } block 159 | && block.Expressions[0] is TryExpression tryExpression 160 | && tryExpression.Handlers is { Count: 0 } 161 | && tryExpression.Finally is MethodCallExpression methodCall 162 | && methodCall.Method.Name is "ExitScriptFunction"; 163 | 164 | if (isInitialTryFinally) 165 | { 166 | return Visit(((TryExpression)((BlockExpression)node.Body).Expressions[0]).Body); 167 | } 168 | 169 | return base.VisitLambda((Expression)node.Reduce()); 170 | } 171 | 172 | protected override Expression VisitBinary(BinaryExpression node) 173 | { 174 | var shouldSkip = _options.IgnoreStartupAndTeardown 175 | && node.NodeType is ExpressionType.Assign 176 | && ( 177 | (node.Left is ParameterExpression parameter && parameter.Name is "context" or "locals") 178 | || (node.Left is MemberExpression member && member.Member.Name is "_functionName")); 179 | 180 | if (shouldSkip) 181 | { 182 | return Expression.Empty(); 183 | } 184 | 185 | shouldSkip = _options.IgnoreQuestionMarkVariable 186 | && node.NodeType is ExpressionType.Assign 187 | && node.Left is MemberExpression member2 && member2.Member.Name is "QuestionMarkVariableValue"; 188 | 189 | if (shouldSkip) 190 | { 191 | return Expression.Empty(); 192 | } 193 | 194 | return base.VisitBinary((BinaryExpression)node.Reduce()); 195 | } 196 | 197 | protected override Expression VisitMethodCall(MethodCallExpression node) 198 | { 199 | if (_options.IgnoreStartupAndTeardown && node.Method.Name is "EnterScriptFunction") 200 | { 201 | return Expression.Empty(); 202 | } 203 | 204 | return base.VisitMethodCall((MethodCallExpression)node.Reduce()); 205 | } 206 | 207 | protected override Expression VisitConditional(ConditionalExpression node) 208 | => base.VisitConditional((ConditionalExpression)node.Reduce()); 209 | 210 | protected override Expression VisitConstant(ConstantExpression node) 211 | => base.VisitConstant((ConstantExpression)node.Reduce()); 212 | 213 | protected override Expression VisitDebugInfo(DebugInfoExpression node) 214 | => base.VisitDebugInfo((DebugInfoExpression)node.Reduce()); 215 | 216 | protected override Expression VisitDefault(DefaultExpression node) 217 | => base.VisitDefault((DefaultExpression)node.Reduce()); 218 | 219 | protected override Expression VisitIndex(IndexExpression node) 220 | => base.VisitIndex((IndexExpression)node.Reduce()); 221 | 222 | protected override Expression VisitInvocation(InvocationExpression node) 223 | => base.VisitInvocation((InvocationExpression)node.Reduce()); 224 | 225 | protected override Expression VisitLabel(LabelExpression node) 226 | => base.VisitLabel((LabelExpression)node.Reduce()); 227 | 228 | protected override Expression VisitListInit(ListInitExpression node) 229 | => base.VisitListInit((ListInitExpression)node.Reduce()); 230 | 231 | protected override Expression VisitLoop(LoopExpression node) 232 | => base.VisitLoop((LoopExpression)node.Reduce()); 233 | 234 | protected override Expression VisitMember(MemberExpression node) 235 | => base.VisitMember((MemberExpression)node.Reduce()); 236 | 237 | protected override Expression VisitMemberInit(MemberInitExpression node) 238 | => base.VisitMemberInit((MemberInitExpression)node.Reduce()); 239 | 240 | protected override Expression VisitNew(NewExpression node) 241 | => base.VisitNew((NewExpression)node.Reduce()); 242 | 243 | protected override Expression VisitNewArray(NewArrayExpression node) 244 | => base.VisitNewArray((NewArrayExpression)node.Reduce()); 245 | 246 | protected override Expression VisitParameter(ParameterExpression node) 247 | => base.VisitParameter((ParameterExpression)node.Reduce()); 248 | 249 | protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node) 250 | => base.VisitRuntimeVariables((RuntimeVariablesExpression)node.Reduce()); 251 | 252 | protected override Expression VisitSwitch(SwitchExpression node) 253 | => base.VisitSwitch((SwitchExpression)node.Reduce()); 254 | 255 | protected override Expression VisitTry(TryExpression node) 256 | => base.VisitTry((TryExpression)node.Reduce()); 257 | 258 | protected override Expression VisitTypeBinary(TypeBinaryExpression node) 259 | => base.VisitTypeBinary((TypeBinaryExpression)node.Reduce()); 260 | 261 | protected override Expression VisitUnary(UnaryExpression node) 262 | => base.VisitUnary((UnaryExpression)node.Reduce()); 263 | } 264 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace ScriptBlockDisassembler 5 | { 6 | internal static class ReflectionExtensions 7 | { 8 | public static T? AccessProperty(this object obj, string name, bool isPublic = false, bool isPrivate = false) 9 | { 10 | return (T?)obj.AccessProperty(name, isPublic, isPrivate); 11 | } 12 | 13 | public static object? AccessProperty( 14 | this object obj, 15 | string name, 16 | bool isPublic = false, 17 | bool isPrivate = false) 18 | { 19 | PropertyInfo? property = obj.GetType().GetProperty( 20 | name, 21 | Flags.Get(isInstance: true, isPublic: isPublic, isPrivate: isPrivate)); 22 | if (property is null) 23 | { 24 | Throw.SomethingChanged($"property '{obj.GetType().FullName}.{name}'"); 25 | } 26 | 27 | return property.GetValue(obj); 28 | } 29 | 30 | public static T? AccessField(this object obj, string name, bool isPublic = false, bool isPrivate = false) 31 | { 32 | return (T?)obj.AccessField(name, isPublic, isPrivate); 33 | } 34 | 35 | public static object? AccessField( 36 | this object obj, 37 | string name, 38 | bool isPublic = false, 39 | bool isPrivate = false) 40 | { 41 | FieldInfo? field = obj.GetType().GetField( 42 | name, 43 | Flags.Get(isInstance: true, isPublic: isPublic, isPrivate: isPrivate)); 44 | if (field is null) 45 | { 46 | Throw.SomethingChanged($"field '{obj.GetType().FullName}.{name}'"); 47 | } 48 | 49 | return field.GetValue(obj); 50 | } 51 | 52 | public static object? InvokePrivateMethod( 53 | this Type type, 54 | string name, 55 | object[]? args = null, 56 | Type[]? argType = null, 57 | bool isPublic = false, 58 | bool isPrivate = false) 59 | { 60 | BindingFlags flags = Flags.Get(isStatic: true, isPublic: isPublic, isPrivate: isPrivate); 61 | MethodInfo? method; 62 | if (argType is not null) 63 | { 64 | method = type.GetMethod(name, flags, argType); 65 | } 66 | else 67 | { 68 | method = type.GetMethod(name, flags); 69 | } 70 | 71 | if (method is null) 72 | { 73 | Throw.SomethingChanged($"method '{type.FullName}.{name}' with a matching signature"); 74 | } 75 | 76 | return method.Invoke(null, args); 77 | } 78 | 79 | public static T? InvokePrivateMethod( 80 | this object obj, 81 | string name, 82 | object[]? args = null, 83 | Type[]? argType = null, 84 | bool isPublic = false, 85 | bool isPrivate = false) 86 | { 87 | return (T?)InvokePrivateMethod(obj, name, args, argType, isPublic, isPrivate); 88 | } 89 | 90 | public static object? InvokePrivateMethod( 91 | this object obj, 92 | string name, 93 | object[]? args = null, 94 | Type[]? argType = null, 95 | bool isPublic = false, 96 | bool isPrivate = false) 97 | { 98 | BindingFlags flags = Flags.Get(isInstance: true, isPublic: isPublic, isPrivate: isPrivate); 99 | MethodInfo? method; 100 | if (argType is not null) 101 | { 102 | method = obj.GetType().GetMethod(name, flags, argType); 103 | } 104 | else 105 | { 106 | method = obj.GetType().GetMethod(name, flags); 107 | } 108 | 109 | if (method is null) 110 | { 111 | Throw.SomethingChanged($"method '{obj.GetType().FullName}.{name}' with a matching signature"); 112 | } 113 | 114 | return method.Invoke(obj, args); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/ScriptBlockDisassembler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/ScriptBlockDisassembler.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'ScriptBlockDisassembler' 3 | # 4 | # Generated by: Patrick Meinecke 5 | # 6 | # Generated on: 11/13/2021 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'ScriptBlockDisassembler.dll' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '1.1.0' 16 | 17 | # ID used to uniquely identify this module 18 | GUID = '32c179e6-6ee8-4ce5-9feb-4962fdb65bb9' 19 | 20 | # Author of this module 21 | Author = 'Patrick Meinecke' 22 | 23 | # Company or vendor of this module 24 | CompanyName = 'Community' 25 | 26 | # Copyright statement for this module 27 | Copyright = '(c) Patrick Meinecke. All rights reserved.' 28 | 29 | # Description of the functionality provided by this module 30 | Description = 'Show a C# representation of what the PowerShell compiler generates for a ScriptBlock.' 31 | 32 | # Minimum version of the PowerShell engine required by this module 33 | PowerShellVersion = '7.2' 34 | 35 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 36 | FunctionsToExport = @() 37 | 38 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 39 | CmdletsToExport = 'Get-ScriptBlockDisassembly', 'Format-ExpressionTree' 40 | 41 | # Variables to export from this module 42 | VariablesToExport = @() 43 | 44 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 45 | AliasesToExport = @() 46 | 47 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 48 | PrivateData = @{ 49 | 50 | PSData = @{ 51 | # Tags applied to this module. These help with module discovery in online galleries. 52 | Tags = 'disasm', 'linq', 'C#' 53 | 54 | # A URL to the license for this module. 55 | LicenseUri = 'https://github.com/SeeminglyScience/ScriptBlockDisassembler/blob/master/LICENSE' 56 | 57 | # A URL to the main website for this project. 58 | ProjectUri = 'https://github.com/SeeminglyScience/ScriptBlockDisassembler' 59 | } # End of PSData hashtable 60 | 61 | } # End of PrivateData hashtable 62 | } 63 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/Throw.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace ScriptBlockDisassembler 6 | { 7 | internal static class Throw 8 | { 9 | [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] 10 | public static void SomethingChanged(string expected) 11 | { 12 | throw new InvalidOperationException( 13 | $"An unsupported implementation detail has unsurprisingly changed. Expected: {expected}"); 14 | } 15 | 16 | [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] 17 | public static void Unreachable() 18 | { 19 | throw new InvalidOperationException("This program location is thought to be unreachable."); 20 | } 21 | 22 | [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] 23 | public static T Unreachable() 24 | { 25 | throw new InvalidOperationException("This program location is thought to be unreachable."); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ScriptBlockDisassembler/TypeIsTranslation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using AgileObjects.ReadableExpressions.Translations; 4 | 5 | namespace ScriptBlockDisassembler 6 | { 7 | internal sealed class TypeIsTranslation : ITranslation 8 | { 9 | private readonly string _typeName; 10 | 11 | private readonly ITranslation _operand; 12 | 13 | private TypeIsTranslation(ITranslation operand, Type type) 14 | { 15 | _typeName = new DynamicStringBuilder(null!).AppendTypeName(type).ToString(); 16 | _operand = operand; 17 | TranslationSize = operand.TranslationSize 18 | + _typeName.Length 19 | + " is ".Length; 20 | 21 | FormattingSize = TranslationSize; 22 | } 23 | 24 | public ExpressionType NodeType => ExpressionType.TypeIs; 25 | 26 | public Type Type => typeof(bool); 27 | 28 | public int TranslationSize { get; } 29 | 30 | public int FormattingSize { get; } 31 | 32 | internal static ITranslation GetTranslationFor(TypeBinaryExpression typeBinary, PSExpressionTranslation translation) 33 | { 34 | return new TypeIsTranslation( 35 | translation.GetTranslationFor(typeBinary.Expression), 36 | typeBinary.TypeOperand); 37 | } 38 | 39 | public int GetIndentSize() => _operand.GetIndentSize(); 40 | 41 | public int GetLineCount() => _operand.GetLineCount(); 42 | 43 | public void WriteTo(TranslationWriter writer) 44 | { 45 | _operand.WriteTo(writer); 46 | writer.WriteToTranslation(" is "); 47 | writer.WriteToTranslation(_typeName); 48 | } 49 | } 50 | } 51 | --------------------------------------------------------------------------------