Saturday, October 08, 2011

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
view raw gistfile1.ps1 hosted with ❤ by GitHub

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 :