What you see vs what you get : matching source and IL with Mono.Cecil and PowerShell
Following on from yesterday, looking at how Mono.Cecil pairs up debug information in the .pdb file with just the sequence points the IL -- which follows the approach taken by coverage tools such as dot-net-coverage. The code is much the same as before:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
This script inspects a method by introspection. | |
.DESCRIPTION | |
This script uses Mono.Cecil introspection to tie 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-MethodWithMonoCecil.ps1 | |
Requires : PowerShell Version 2.0 (3.0 or alternative launcher for .net 4/FxCop 10.0) | |
.EXAMPLE | |
.\Inspect-MethodWithMonoCecil.ps1 -CecilPath ...\Mono.Cecil -AssemblyPath ...\Tinesware.Rules.dll | |
-ClassName Tinesware.Rules.SpellCheckRule | |
-MethodName VisitField | ? { $_.Context -ne "<None>" } | Out-GridView | |
.PARAMETER CecilPath | |
The path to the directory where Mono.Cecil.*.dll have 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 | |
#> | |
param ( | |
[Parameter(Mandatory = $true)] [string] $CecilPath, | |
[Parameter(Mandatory = $true)] [string] $AssemblyPath, | |
[Parameter(Mandatory = $true)] [string] $ClassName, | |
[Parameter(Mandatory = $true)] [string] $MethodName, | |
[switch] $Help) | |
if ($help -or (-not (Test-Path $CecilPath -PathType Container)) -or | |
(-not (Test-Path $AssemblyPath -PathType Leaf)) -or | |
(-not (Test-Path $(Join-Path -Path $CecilPath -ChildPath 'Mono.Cecil.dll') -PathType Leaf))) | |
{ | |
Get-Help $MyInvocation.MyCommand.Definition | |
return | |
} | |
Add-Type -Path $(Join-Path -Path $CecilPath -ChildPath 'Mono.Cecil.dll') | |
Add-Type -Path $(Join-Path -Path $CecilPath -ChildPath 'Mono.Cecil.Pdb.dll') | |
# load assembly with debug symbols | |
# We could reflect into $assembly to get the built-in symbol path, which is messy | |
# In most case it's safe to assume the files are co-located | |
$assembly = [Mono.Cecil.AssemblyDefinition]::ReadAssembly($AssemblyPath) | |
$symbolPath = [System.IO.Path]::ChangeExtension($assembly.MainModule.FullyQualifiedName, ".pdb") | |
$provider = New-Object Mono.Cecil.Pdb.PdbReaderProvider | |
$reader = $provider.GetSymbolReader($assembly.MainModule, $symbolPath) | |
$assembly.MainModule.ReadSymbols($reader) | |
# load the type | |
$splitnames = @($ClassName.Split('+')) | |
$types = @($assembly.Modules | % { $_.Types }) | |
$type = $types | ? { $_.FullName -eq $splitnames[0] } | Select-Object -First 1 | |
# handle nested types TODO | |
if (($splitnames.Length -gt 1) -and $type) { | |
$splitnames[1..$($splitnames.Length-1)] | % { | |
$name = $_ | |
if ($type) { $type = $type.NestedTypes | ? { $_.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.Methods | ? { $_.Name -eq $MethodName }) | |
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 | % { | |
$parameters = @($_.Parameters | % { "$($_.ParameterType) $($_.Name)" } ) | |
$parameters = $parameters -join ", " | |
[string] "&$($index)$([char]8)$($_.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.Body.Instructions | % { $_.SequencePoint } | ? { $_ } | % { $_.Document } | ? { $_ } | % { $_.Url } | ? { $_ } | Select-Object -First 1 | |
$sourceListing = Get-Content $source | |
# Source context to code snippet | |
Function Extract-Snippet ([Mono.Cecil.Cil.SequencePoint] $context) | |
{ | |
if ( (-not $context.Document) -or (-not $context.Document.Url) -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) | |
} | |
} | |
## Report Instructions | |
$method.Body.Instructions | % { | |
$sequencePoint = @($_.SequencePoint.StartLine, $_.SequencePoint.StartColumn, $_.SequencePoint.EndLine, $_.SequencePoint.EndColumn) | |
if (-not $_.SequencePoint.EndLine) { $sequencePoint = "null" } | |
if ($_.SequencePoint.StartLine -ge $sourceListing.Length) { $sequencePoint = "0xfeefee" } | |
New-Object PSObject -Property @{ | |
OpCode = $_.OpCode; | |
Context = [System.String]::Join("`r`n", (Extract-Snippet $_.SequencePoint)); | |
Details = $_; | |
SequencePoint = $sequencePoint | |
} | |
} |
with the addition of the line/column data in SequencePoint ("null" if the values are not given, "0xfeefee" for the well-known compiler generated fake line number).
No comments :
Post a Comment