├── .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 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
--------------------------------------------------------------------------------