What you see vs what you get : matching source and IL with FxCop and PowerShell
The mapping between the code you write and the object code that gets generated is not always a simple one; F# code in particular is heavily restructured in the compilation phase. So when trying to perform code analysis to detect source level artefacts in the compiled code, some guide is useful to navigate between the two representations.
So, here is a PowerShell script to do just that, looking at the IL instructions or logical statements for a method and the corresponding source code as given via the .pdb debugging information -- PowerShell rather than F# interactive because the former has a much nicer user interaction model, especially when asking the user to resolve which overloaded method name was really intended.
<# | |
.SYNOPSIS | |
This script inspects a method by introspection. | |
.DESCRIPTION | |
This script uses FxCop introspection to tie statements or instructions with source. | |
It loads the referenced assemblies into the current AppDomain, so should be run in a | |
separate process to your main script | |
.NOTES | |
File Name : Inspect-Method.ps1 | |
Requires : PowerShell Version 2.0 (3.0 or alternative launcher for .net 4/FxCop 10.0) | |
.PARAMETER FxCopPath | |
The path to the directory where FxCop has been installed | |
.PARAMETER AssemblyPath | |
The path to the assembly to inspect | |
.PARAMETER ClassName | |
The name of the class containg the Method. Nested types are separated with + e.g. | |
Project.Namespace.Type+NestedType | |
.PARAMETER MethodName | |
The name of the method to inspect | |
.PARAMETER Instructions | |
By default, the output matches FxCop statements to source; with this switch, matches | |
IL instructions to the source. | |
#> | |
param ( | |
[string] $FxCopPath, | |
[Parameter(Mandatory = $true)] [string] $AssemblyPath, | |
[Parameter(Mandatory = $true)] [string] $ClassName, | |
[Parameter(Mandatory = $true)] [string] $MethodName, | |
[switch] $Instructions, | |
[switch] $Help) | |
$fxcopVersion = @{ "v2"='Microsoft FxCop 1.36'; "v4"='Microsoft FxCop 10.0' } | |
if (-not $FxCopPath) { | |
$programFiles = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFiles) | |
$dotNetVersion = [System.Runtime.InteropServices.RuntimeEnvironment]::GetSystemVersion().Split('.') | Select-Object -First 1 | |
$FxCopPath = Join-Path -Path $programFiles -ChildPath ($fxcopVersion[$dotNetVersion]) | |
} | |
if ($help -or (-not (Test-Path $FxCopPath -PathType Container)) -or | |
(-not (Test-Path $AssemblyPath -PathType Leaf)) -or | |
(-not (Test-Path $(Join-Path -Path $FxCopPath -ChildPath 'FxCopSdk.dll') -PathType Leaf))) | |
{ | |
Get-Help $MyInvocation.MyCommand.Definition | |
return | |
} | |
# load assembly with debug symbols | |
Add-Type -Path $(Join-Path -Path $FxCopPath -ChildPath 'FxCopSdk.dll') | |
$assembly = [Microsoft.FxCop.Sdk.AssemblyNode]::GetAssembly($AssemblyPath, $null, $false, $true, $true, $false) | |
# load the type | |
$splitnames = @($ClassName.Split('+')) | |
$type = $assembly.Types | ? { $_.FullName -eq $splitnames[0] } | Select-Object -First 1 | |
# handle nested types | |
if (($splitnames.Length -gt 1) -and $type) { | |
$splitnames[1..$($splitnames.Length-1)] | % { | |
$name = $_ | |
if ($type) { $type = $type.NestedTypes | ? { $_.Name.Name -eq $name } | Select-Object -First 1 } | |
} | |
} | |
if (-not $type) { | |
Write-Error "Class named '$ClassName' not found" -Category ObjectNotFound -TargetObject $ClassName | |
return | |
} | |
# load the method, being ready to resolve overloads | |
$methods = @($type.Members | ? { ($_.Name.Name -eq $MethodName) -and ($_.NodeType -eq [Microsoft.FxCop.Sdk.NodeType]::Method) }) | |
if (-not $methods) { | |
Write-Error "Method named '$MethodName' not found" -Category ObjectNotFound -TargetObject "$ClassName.$MethodName" | |
return | |
} | |
# User choice adapted from http://blogs.technet.com/b/jamesone/archive/2009/06/24/how-to-get-user-input-more-nicely-in-powershell.aspx | |
Function Select-Item | |
{ | |
Param( [String]$Caption="Please make a selection", | |
[String]$Message="Choices are presented below", | |
[String[]]$choiceList, | |
[int]$default=0) | |
$choicedesc = New-Object System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription] | |
$choiceList | foreach { $choicedesc.Add((New-Object "System.Management.Automation.Host.ChoiceDescription" -ArgumentList $_))} | |
$Host.ui.PromptForChoice($caption, $message, $choicedesc, $default) | |
} | |
$index = 0 | |
if ($methods.Length -gt 1) { | |
$choices = @($methods | % { | |
[string] "&$($index)$([char]8)$($_.Name.Name)($($_.Parameters))" | |
$index += 1 | |
}) | |
$index = Select-Item "Choose which overload to use" "The various overloads are:" $choices 0 | |
} | |
# Overloads resolved, get the source | |
$method = $methods[$index] | |
$source = $method.Instructions | % { $_.SourceContext.FileName } | ? { $_ } | Select-Object -First 1 | |
$sourceListing = Get-Content $source | |
# Source context to code snippet | |
Function Extract-Snippet ([Microsoft.FxCop.Sdk.SourceContext] $context, [string] $prefix="") | |
{ | |
if ( (-not $context.FileName) -or (-not $context.EndLine) -or ($context.StartLine -ge $sourceListing.Length)) { | |
"<None>" | |
} else { | |
$snippet = @($sourceListing[$($context.StartLine-1)..$($context.EndLine-1)]) | |
$snippet[0] = (" " * ($context.StartColumn-1)) + $snippet[0].SubString($context.StartColumn-1) | |
$snippet[-1] = $snippet[-1].SubString(0, $context.EndColumn-1) | |
$snippet | % { $prefix + $_ } | |
} | |
} | |
## Report Instructions if requested, then exit | |
if ($Instructions) { | |
$method.Instructions | % { | |
New-Object PSObject -Property @{ | |
OpCode = $_.OpCode; | |
Context = [System.String]::Join("`r`n", (Extract-Snippet $_.SourceContext)); | |
Details = $_ | |
} | |
} | |
return | |
} | |
# Expand a StatementCollection recursively, indicating block structure with levels of ">") | |
Function Expand-Block([Microsoft.FxCop.Sdk.StatementCollection] $block, [string] $prefix="") { | |
$block | % { | |
New-Object PSObject -Property @{ | |
NodeType = $_.NodeType; | |
Context = [System.String]::Join("`r`n", (Extract-Snippet $_.SourceContext $prefix)); | |
Details = $_ | |
} | |
## recurse into blocks | |
if ($_.NodeType -eq [Microsoft.FxCop.Sdk.NodeType]::Block) { Expand-Block $_.Statements ($prefix+">") } | |
} | |
} | |
## Report Statements | |
Expand-Block $method.Body.Statements |
As the script writes rich objects in the successful output to pipeline, it can be used as a starting point for further analysis; or the output can be piped into e.g. Out-GridView
for inspection.
No comments :
Post a Comment