Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Verify.Cecil

Discussions Build status NuGet Status NuGet Status

Extends Verify to allow snapshot testing and structural validation of Mono.Cecil modules, types and members. An alternative to PEVerify.

See Milestones for release notes.

Sponsors

Entity Framework Extensions

Entity Framework Extensions is a major sponsor and is proud to contribute to the development this project.

Entity Framework Extensions

Developed using JetBrains IDEs

JetBrains logo.

NuGet

There are two packages, built from the same source. Use the one that matches the Mono.Cecil already in the test project.

The Mono.Cecil bundled in FodyHelpers is signed with a different key to the Mono.Cecil package, so the two cannot be mixed. A test project that references FodyHelpers should also avoid the TUnit metapackage, since it brings in the Mono.Cecil package. Reference TUnit.Core, TUnit.Engine and TUnit.Assertions instead.

Usage

[ModuleInitializer]
public static void Init() =>
    VerifyCecil.Initialize();

snippet source | anchor

The following Cecil types can be verified: AssemblyDefinition, ModuleDefinition, TypeDefinition, MethodDefinition, FieldDefinition, PropertyDefinition and EventDefinition.

The output is ILAsm-like text. It is deterministic: RVAs, metadata tokens, MVIDs and the source revision in AssemblyInformationalVersionAttribute are excluded, and instruction offsets are computed from the instructions. So a method modified in memory is shown with the offsets it will have when written.

Given the following type:

public class Target :
    INotifyPropertyChanged
{
    void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
        PropertyChanged?.Invoke(this, new(propertyName));

    public event PropertyChangedEventHandler? PropertyChanged;

    string? property;

    public string? Property
    {
        get => property;
        set
        {
            property = value;
            OnPropertyChanged();
        }
    }
}

snippet source | anchor

Verify a type

[Test]
public async Task TypeUsage()
{
    using var module = ModuleDefinition.ReadModule(assemblyPath);
    await Verify(module.GetType("Target"));
}

snippet source | anchor

Result:

.class public auto ansi beforefieldinit Target
  extends [System.Runtime]System.Object
  implements [System.ObjectModel]System.ComponentModel.INotifyPropertyChanged
{
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (2)
  .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (0)
  .interfaceimpl type [System.ObjectModel]System.ComponentModel.INotifyPropertyChanged
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (0)
  .field private class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler PropertyChanged
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ([System.Runtime]System.Diagnostics.DebuggerBrowsableState(0))
  .field private string property

  .method private hidebysig instance void OnPropertyChanged([opt] string propertyName) cil managed
  {
    .param [1] = nullref
      .custom instance void [System.Runtime]System.Runtime.CompilerServices.CallerMemberNameAttribute::.ctor()
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
    IL_0006: dup
    IL_0007: brtrue.s IL_000c
    IL_0009: pop
    IL_000a: br.s IL_0019
    IL_000c: ldarg.0
    IL_000d: ldarg.1
    IL_000e: newobj instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs::.ctor(string)
    IL_0013: callvirt instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler::Invoke(object, class [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs)
    IL_0018: nop
    IL_0019: ret
  }

  .method public hidebysig newslot specialname virtual final instance void add_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler value) cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .locals init (
      [0] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
      [1] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
      [2] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
    )

    IL_0000: ldarg.0
    IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: stloc.1
    IL_0009: ldloc.1
    IL_000a: ldarg.1
    IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Combine(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate)
    IL_0010: castclass [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
    IL_0015: stloc.2
    IL_0016: ldarg.0
    IL_0017: ldflda class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
    IL_001c: ldloc.2
    IL_001d: ldloc.1
    IL_001e: call !!0 [System.Threading]System.Threading.Interlocked::CompareExchange<class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler>(!!0&, !!0, !!0)
    IL_0023: stloc.0
    IL_0024: ldloc.0
    IL_0025: ldloc.1
    IL_0026: bne.un.s IL_0007
    IL_0028: ret
  }

  .method public hidebysig newslot specialname virtual final instance void remove_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler value) cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .locals init (
      [0] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
      [1] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
      [2] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
    )

    IL_0000: ldarg.0
    IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: stloc.1
    IL_0009: ldloc.1
    IL_000a: ldarg.1
    IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Remove(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate)
    IL_0010: castclass [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
    IL_0015: stloc.2
    IL_0016: ldarg.0
    IL_0017: ldflda class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
    IL_001c: ldloc.2
    IL_001d: ldloc.1
    IL_001e: call !!0 [System.Threading]System.Threading.Interlocked::CompareExchange<class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler>(!!0&, !!0, !!0)
    IL_0023: stloc.0
    IL_0024: ldloc.0
    IL_0025: ldloc.1
    IL_0026: bne.un.s IL_0007
    IL_0028: ret
  }

  .method public hidebysig specialname instance string get_Property() cil managed
  {
    IL_0000: ldarg.0
    IL_0001: ldfld string Target::property
    IL_0006: ret
  }

  .method public hidebysig specialname instance void set_Property(string value) cil managed
  {
    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldarg.1
    IL_0003: stfld string Target::property
    IL_0008: ldarg.0
    IL_0009: ldstr "Property"
    IL_000e: call instance void Target::OnPropertyChanged(string)
    IL_0013: nop
    IL_0014: ret
  }

  .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
  {
    IL_0000: ldarg.0
    IL_0001: call instance void [System.Runtime]System.Object::.ctor()
    IL_0006: nop
    IL_0007: ret
  }

  .property instance string Property()
  {
    .get instance string Target::get_Property()
    .set instance void Target::set_Property(string)
  }

  .event [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler PropertyChanged
  {
    .addon instance void Target::add_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler)
    .removeon instance void Target::remove_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler)
  }
}

snippet source | anchor

Verify a method

[Test]
public async Task MethodUsage()
{
    using var module = ModuleDefinition.ReadModule(assemblyPath);
    var type = module.GetType("Target");
    await Verify(type.Methods.Single(_ => _.Name == "OnPropertyChanged"));
}

snippet source | anchor

Result:

.method private hidebysig instance void OnPropertyChanged([opt] string propertyName) cil managed
{
  .param [1] = nullref
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CallerMemberNameAttribute::.ctor()
  IL_0000: ldarg.0
  IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
  IL_0006: dup
  IL_0007: brtrue.s IL_000c
  IL_0009: pop
  IL_000a: br.s IL_0019
  IL_000c: ldarg.0
  IL_000d: ldarg.1
  IL_000e: newobj instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs::.ctor(string)
  IL_0013: callvirt instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler::Invoke(object, class [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs)
  IL_0018: nop
  IL_0019: ret
}

snippet source | anchor

Verify a property

[Test]
public async Task PropertyUsage()
{
    using var module = ModuleDefinition.ReadModule(assemblyPath);
    var type = module.GetType("Target");
    await Verify(type.Properties.Single(_ => _.Name == "Property"));
}

snippet source | anchor

Result:

.property instance string Property()
{
  .get instance string Target::get_Property()
  .set instance void Target::set_Property(string)
}

snippet source | anchor

Verify a module

[Test]
public async Task ModuleUsage()
{
    using var module = ModuleDefinition.ReadModule(assemblyPath);
    await Verify(module);
}

snippet source | anchor

Validation

A snapshot detects changes, but not IL that is invalid. So before converting, the IL is checked, and verification fails with a CecilValidationException listing the problems. This covers the problems PEVerify commonly reports for woven assemblies:

  • Branch and switch targets that are not in the method body.
  • Short branches whose target is out of range. Cecil silently writes a wrong offset for these.
  • Stack underflow, differing stack depths where paths merge, items left on the stack at ret, and a non-empty stack on entry to a try.
  • Control falling through the end of the method body, and empty method bodies.
  • Exception handlers with boundaries missing or outside the body, empty or overlapping ranges, and catch handlers without a catch type.
  • Operands of the wrong type, arguments and locals that do not exist, and variables or parameters from another method.
  • Method, field and type references that do not resolve, and references that do not match the definition: static vs instance fields and methods, newobj on a method that is not a constructor, and callvirt on a static method.
  • Abstract methods in types that are not abstract.

References are only checked when the assembly containing them can be found. If it cannot, the check is skipped rather than reported.

[Test]
public async Task ValidationThrows()
{
    using var module = ModuleDefinition.ReadModule(assemblyPath);
    var method = AddMethod(
        module,
        module.TypeSystem.Void,
        il =>
        {
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Ret);
        });

    await Assert.ThrowsAsync<CecilValidationException>(
        async () => await Verify(method));
}

snippet source | anchor

To snapshot without validating:

[Test]
public async Task DisableCecilValidation()
{
    using var module = ModuleDefinition.ReadModule(assemblyPath);
    var method = AddMethod(
        module,
        module.TypeSystem.Void,
        il =>
        {
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Ret);
        });

    await Verify(method)
        .DisableCecilValidation();
}

snippet source | anchor

CecilValidator can also be used directly:

IReadOnlyList<string> problems = CecilValidator.Validate(module);
CecilValidator.ThrowIfInvalid(module);

Replacing PEVerify in Fody weaver tests

PEVerify only runs on .NET Framework and needs the Windows SDK installed. Verify.Cecil.FodyHelpers runs on any platform and target framework.

Disable PEVerify in ExecuteTestRun and verify the woven types:

[Test]
public async Task WeaverUsage()
{
    var weaver = new ModuleWeaver();
    var result = weaver.ExecuteTestRun(
        "AssemblyToProcess.dll",
        // Verify.Cecil replaces PEVerify
        runPeVerify: false);

    using var module = ModuleDefinition.ReadModule(result.AssemblyPath);
    await Verify(module.GetType("Target"));
}

snippet source | anchor

A weaver that produces invalid IL fails the test:

[Test]
public async Task BrokenWeaver()
{
    var weaver = new ModuleWeaver
    {
        Broken = true
    };
    var result = weaver.ExecuteTestRun(
        "AssemblyToProcess.dll",
        runPeVerify: false,
        assemblyName: "BrokenWeaver");

    using var module = ModuleDefinition.ReadModule(result.AssemblyPath);
    var exception = await Assert.ThrowsAsync<CecilValidationException>(
        async () => await Verify(module.GetType("Target")));

    await Assert.That(exception!.Problems)
        .IsEquivalentTo(
        [
            "System.String Target::Injected(): " +
            "IL_0006: ret: stack must be empty after ret, but has 1 left"
        ]);
}

snippet source | anchor

Icon

Helmet designed by Leonidas Ikonomou from The Noun Project.

About

No description, website, or topics provided.

Resources

Code of conduct

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages