Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ jobs:

- uses: actions/setup-dotnet@v5
with:
dotnet-version: '11.0.x'
# 10.0.x: runtime for the net10.0 unit test project.
dotnet-version: |
10.0.x
11.0.x
dotnet-quality: 'preview'

- name: Add msbuild to PATH
Expand All @@ -36,6 +39,9 @@ jobs:
- name: Build and pack
run: dotnet build ReferenceAssemblyAnnotator.slnx -c $env:Configuration

- name: Unit tests
run: dotnet test --project ICSharpCode.ReferenceAssemblyAnnotator.Tests -c $env:Configuration --no-build

- name: Test (dotnet msbuild)
run: ./build/test.ps1 -Configuration $env:Configuration -Engine dotnet -PackagesPath $env:TestPackages

Expand Down
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ dotnet build ReferenceAssemblyAnnotator.slnx -c Release

## Tests

There is no unit test project. The tests are sample projects under `tests/` that must build cleanly with warnings as errors against the locally built package. Build the solution first, then:
`ICSharpCode.ReferenceAssemblyAnnotator.Tests` (xUnit v3 on Microsoft Testing Platform, net10.0) unit-tests the comparers and `Program.Annotate`/`AnnotateType` on in-memory Cecil modules built with `CecilBuilder`; it sees the weaver's internals via `InternalsVisibleTo`. `global.json` selects the MTP runner for `dotnet test`. After building the solution:

```
dotnet test --project ICSharpCode.ReferenceAssemblyAnnotator.Tests -c Release --no-build
```

The integration tests are sample projects under `tests/` that must build cleanly with warnings as errors against the locally built package. Build the solution first, then:

```
./build/test.ps1 -Configuration Release -Engine dotnet # dotnet msbuild
Expand All @@ -39,7 +45,7 @@ dotnet msbuild -restore tests/SingleTFM -p:TestFramework=netstandard2.0 -warnase
- The test projects leave out the `Microsoft.NETCore.App.Ref` `PackageDownload` for .NET Core 3.0+ (netcoreapp3.x, net10.0). Those aren't annotated, and the SDK adds its own download whenever the targeting pack isn't installed, so a second one fails with NU1505. The `GetHashCode` line in the test sources is `#if !NETCOREAPP3_0_OR_GREATER` because only annotated TFMs get `[DisallowNull]` removed (`Program.IsExcludedAnnotation`).
- Keep the local source path in `tests/NuGet.Config` with forward slashes: with backslashes, source mapping doesn't match it on Linux/macOS (NU1100).

CI: `.github/workflows/build.yml` runs the same build and test script with both engines (Release only) and uploads the nupkg. On the runner, paths in the isolated cache exceed MAX_PATH under `msbuild.exe`. So the workflow passes `-PackagesPath` with a short path outside the checkout; that cache isn't wiped between cases, which is safe because a run only ever contains one annotator version. `test.ps1` also passes `core.longpaths=true` to `git clean`.
CI: `.github/workflows/build.yml` runs the same build, the unit tests and the test script with both engines (Release only) and uploads the nupkg. On the runner, paths in the isolated cache exceed MAX_PATH under `msbuild.exe`. So the workflow passes `-PackagesPath` with a short path outside the checkout; that cache isn't wiped between cases, which is safe because a run only ever contains one annotator version. `test.ps1` also passes `core.longpaths=true` to `git clean`.

## Architecture

Expand Down
101 changes: 101 additions & 0 deletions ICSharpCode.ReferenceAssemblyAnnotator.Tests/AnnotateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace ICSharpCode.ReferenceAssemblyAnnotator.Tests
{
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Rocks;
using Xunit;
using static CecilBuilder;

public class AnnotateTests
{
private readonly ModuleDefinition _module = CreateModule("Unannotated");
private readonly ModuleDefinition _annotatedModule = CreateModule("Annotated");
private readonly TypeDefinition _nullable;
private readonly TypeDefinition _annotatedNullable;
private readonly TypeDefinition _disallowNull;
private readonly TypeDefinition _annotatedDisallowNull;
private readonly Dictionary<string, TypeDefinition> _attributesOfInterest;

public AnnotateTests()
{
// As in Program.Main, the attributes of interest are types the unannotated module can reference.
_nullable = AddAttributeType(_module, NullableAttribute, _module.TypeSystem.Byte);
_annotatedNullable = AddAttributeType(_annotatedModule, NullableAttribute, _annotatedModule.TypeSystem.Byte);
_disallowNull = AddAttributeType(_module, DisallowNullAttribute);
_annotatedDisallowNull = AddAttributeType(_annotatedModule, DisallowNullAttribute);
_attributesOfInterest = new Dictionary<string, TypeDefinition>
{
[NullableAttribute] = _nullable,
[DisallowNullAttribute] = _disallowNull,
};
}

[Fact]
public void AnnotateType_MatchesByRefGenericOverloads()
{
// Regression: these overloads used to compare equal, so FindMatchingMethod threw.
var type = AddType(_module, "N", "C");
var boolOverload = AddMethod(type, "M", Nullable(_module, _module.TypeSystem.Boolean).MakeByReferenceType());
var byteOverload = AddMethod(type, "M", Nullable(_module, _module.TypeSystem.Byte).MakeByReferenceType());

var annotatedType = AddType(_annotatedModule, "N", "C");
var annotatedBool = AddMethod(annotatedType, "M", Nullable(_annotatedModule, _annotatedModule.TypeSystem.Boolean).MakeByReferenceType());
var annotatedByte = AddMethod(annotatedType, "M", Nullable(_annotatedModule, _annotatedModule.TypeSystem.Byte).MakeByReferenceType());
AddAttribute(annotatedBool.Parameters[0], _annotatedNullable, NullableFlag(1));
AddAttribute(annotatedByte.Parameters[0], _annotatedNullable, NullableFlag(2));

Program.AnnotateType(null, _module, type, _annotatedModule, _attributesOfInterest);

Assert.Equal((byte)1, SingleNullableFlag(boolOverload.Parameters[0]));
Assert.Equal((byte)2, SingleNullableFlag(byteOverload.Parameters[0]));
}

[Fact]
public void Annotate_ReplacesAttributesOfInterestAndKeepsOthers()
{
var keep = AddAttributeType(_module, "N.KeepAttribute");
var parameter = AddMethod(AddType(_module, "N", "C"), "M", _module.TypeSystem.String).Parameters[0];
AddAttribute(parameter, _nullable, NullableFlag(1));
AddAttribute(parameter, keep);

var annotatedParameter = AddMethod(AddType(_annotatedModule, "N", "C"), "M", _annotatedModule.TypeSystem.String).Parameters[0];
AddAttribute(annotatedParameter, _annotatedNullable, NullableFlag(2));

Program.Annotate(_module, parameter, annotatedParameter, _attributesOfInterest);

Assert.Equal((byte)2, SingleNullableFlag(parameter));
Assert.Contains(parameter.CustomAttributes, attribute => attribute.AttributeType == keep);
Assert.Equal(2, parameter.CustomAttributes.Count);
}

[Theory]
[InlineData("EqualityComparer`1")]
[InlineData("IEqualityComparer`1")]
public void AnnotateType_DropsDisallowNullOnGetHashCode(string typeName)
{
var type = AddType(_module, "System.Collections.Generic", typeName, genericArity: 1);
var getHashCode = AddMethod(type, "GetHashCode", type.GenericParameters[0]);
AddAttribute(getHashCode.Parameters[0], _disallowNull);

var annotatedType = AddType(_annotatedModule, "System.Collections.Generic", typeName, genericArity: 1);
var annotatedParameter = AddMethod(annotatedType, "GetHashCode", annotatedType.GenericParameters[0]).Parameters[0];
AddAttribute(annotatedParameter, _annotatedDisallowNull);
AddAttribute(annotatedParameter, _annotatedNullable, NullableFlag(1));

Program.AnnotateType(null, _module, type, _annotatedModule, _attributesOfInterest);

Assert.DoesNotContain(getHashCode.Parameters[0].CustomAttributes, attribute => attribute.AttributeType.FullName == DisallowNullAttribute);
Assert.Equal((byte)1, SingleNullableFlag(getHashCode.Parameters[0]));
}

private static byte SingleNullableFlag(ICustomAttributeProvider provider)
=> (byte)provider.CustomAttributes.Single(attribute => attribute.AttributeType.FullName == NullableAttribute).ConstructorArguments[0].Value;

private CustomAttributeArgument NullableFlag(byte value)
=> new CustomAttributeArgument(_annotatedModule.TypeSystem.Byte, value);
}
}
78 changes: 78 additions & 0 deletions ICSharpCode.ReferenceAssemblyAnnotator.Tests/CecilBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace ICSharpCode.ReferenceAssemblyAnnotator.Tests
{
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Rocks;

/// <summary>
/// Builds small in-memory Cecil modules for the tests.
/// </summary>
internal static class CecilBuilder
{
public const string NullableAttribute = "System.Runtime.CompilerServices.NullableAttribute";
public const string DisallowNullAttribute = "System.Diagnostics.CodeAnalysis.DisallowNullAttribute";

public static ModuleDefinition CreateModule(string name)
=> ModuleDefinition.CreateModule(name, ModuleKind.Dll);

public static TypeDefinition AddType(ModuleDefinition module, string @namespace, string name, int genericArity = 0)
{
var type = new TypeDefinition(@namespace, name, TypeAttributes.Public | TypeAttributes.Class, module.TypeSystem.Object);
for (int i = 0; i < genericArity; i++)
type.GenericParameters.Add(new GenericParameter("T" + i, type));

module.Types.Add(type);
return type;
}

public static MethodDefinition AddMethod(TypeDefinition type, string name, params TypeReference[] parameterTypes)
{
var method = new MethodDefinition(name, MethodAttributes.Public, type.Module.TypeSystem.Void);
foreach (var parameterType in parameterTypes)
method.Parameters.Add(new ParameterDefinition(parameterType));

type.Methods.Add(method);
return method;
}

// Adds an attribute type with a constructor taking the given parameter types.
public static TypeDefinition AddAttributeType(ModuleDefinition module, string fullName, params TypeReference[] constructorParameterTypes)
{
int dot = fullName.LastIndexOf('.');
var type = AddType(module, fullName.Substring(0, dot), fullName.Substring(dot + 1));
var constructor = new MethodDefinition(
".ctor",
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
module.TypeSystem.Void);
foreach (var parameterType in constructorParameterTypes)
constructor.Parameters.Add(new ParameterDefinition(parameterType));

type.Methods.Add(constructor);
return type;
}

public static void AddAttribute(ICustomAttributeProvider provider, TypeDefinition attributeType, params CustomAttributeArgument[] arguments)
{
var attribute = new CustomAttribute(attributeType.Methods.Single(method => method.IsConstructor));
foreach (var argument in arguments)
attribute.ConstructorArguments.Add(argument);

provider.CustomAttributes.Add(attribute);
}

public static TypeReference Nullable(ModuleDefinition module, TypeReference typeArgument)
=> Generic(module, "System", "Nullable`1", isValueType: true, typeArgument);

public static TypeReference Generic(ModuleDefinition module, string @namespace, string name, bool isValueType, params TypeReference[] typeArguments)
{
var open = new TypeReference(@namespace, name, module, module.TypeSystem.CoreLibrary, isValueType);
for (int i = 0; i < typeArguments.Length; i++)
open.GenericParameters.Add(new GenericParameter(open));

return open.MakeGenericInstanceType(typeArguments);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

namespace ICSharpCode.ReferenceAssemblyAnnotator.Tests
{
using Mono.Cecil;
using Mono.Cecil.Rocks;
using Xunit;
using static CecilBuilder;

public class EquivalenceComparersTests
{
private readonly ModuleDefinition _module = CreateModule("A");
private readonly ModuleDefinition _otherModule = CreateModule("B");

[Fact]
public void ByReference_DistinguishesGenericArguments()
{
// Regression: GetElementType() on Nullable<bool>& returns the open Nullable`1.
var x = Nullable(_module, _module.TypeSystem.Boolean).MakeByReferenceType();
var y = Nullable(_module, _module.TypeSystem.Byte).MakeByReferenceType();

Assert.False(EquivalenceComparers.TypeReference.Equals(x, y));
}

[Fact]
public void Pointer_DistinguishesGenericArguments()
{
var x = Nullable(_module, _module.TypeSystem.Boolean).MakePointerType();
var y = Nullable(_module, _module.TypeSystem.Byte).MakePointerType();

Assert.False(EquivalenceComparers.TypeReference.Equals(x, y));
}

[Fact]
public void Array_DistinguishesGenericArguments()
{
var x = Nullable(_module, _module.TypeSystem.Boolean).MakeArrayType();
var y = Nullable(_module, _module.TypeSystem.Byte).MakeArrayType();

Assert.False(EquivalenceComparers.TypeReference.Equals(x, y));
}

[Fact]
public void Array_DistinguishesRank()
{
var x = _module.TypeSystem.Int32.MakeArrayType(1);
var y = _module.TypeSystem.Int32.MakeArrayType(2);

Assert.False(EquivalenceComparers.TypeReference.Equals(x, y));
}

[Fact]
public void SameShapeFromDifferentModules_AreEqual()
{
var x = Nullable(_module, _module.TypeSystem.Boolean).MakeByReferenceType().MakeArrayType();
var y = Nullable(_otherModule, _otherModule.TypeSystem.Boolean).MakeByReferenceType().MakeArrayType();

Assert.True(EquivalenceComparers.TypeReference.Equals(x, y));
Assert.Equal(EquivalenceComparers.TypeReference.GetHashCode(x), EquivalenceComparers.TypeReference.GetHashCode(y));
}

[Fact]
public void Nested_DistinguishedFromTopLevel()
{
var outer = AddType(_module, "N", "Outer");
var nested = new TypeDefinition(string.Empty, "Inner", TypeAttributes.NestedPublic, _module.TypeSystem.Object);
outer.NestedTypes.Add(nested);
var topLevel = AddType(_otherModule, string.Empty, "Inner");

Assert.False(EquivalenceComparers.TypeReference.Equals(nested, topLevel));
Assert.False(EquivalenceComparers.TypeReference.Equals(topLevel, nested));
}

[Fact]
public void GenericArity_Matters()
{
var x = AddType(_module, "N", "C");
var y = AddType(_otherModule, "N", "C", genericArity: 1);

Assert.False(EquivalenceComparers.TypeReference.Equals(x, y));
Assert.False(EquivalenceComparers.TypeDefinition.Equals(x, y));
}

[Fact]
public void MethodDefinition_DistinguishesByRefGenericOverloads()
{
var type = AddType(_module, "N", "C");
var boolOverload = AddMethod(type, "M", Nullable(_module, _module.TypeSystem.Boolean).MakeByReferenceType());
var byteOverload = AddMethod(type, "M", Nullable(_module, _module.TypeSystem.Byte).MakeByReferenceType());

var otherType = AddType(_otherModule, "N", "C");
var otherBoolOverload = AddMethod(otherType, "M", Nullable(_otherModule, _otherModule.TypeSystem.Boolean).MakeByReferenceType());

Assert.True(EquivalenceComparers.MethodDefinition.Equals(boolOverload, otherBoolOverload));
Assert.False(EquivalenceComparers.MethodDefinition.Equals(byteOverload, otherBoolOverload));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
</PropertyGroup>

<PropertyGroup>
<CodeAnalysisRuleSet>..\ICSharpCode.ReferenceAssemblyAnnotator.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" Version="4.0.3" />
<!-- net10.0 references aren't annotated, and the SDK adds its own targeting pack download when it's missing (NU1505). -->
<PackageDownload Remove="Microsoft.NETCore.App.Ref" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ICSharpCode.ReferenceAssemblyAnnotator\ICSharpCode.ReferenceAssemblyAnnotator.csproj" />
</ItemGroup>

</Project>
4 changes: 2 additions & 2 deletions ICSharpCode.ReferenceAssemblyAnnotator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private static void AnnotateModule(SuppressibleLoggingHelper? log, ModuleDefinit
}
}

private static void AnnotateType(SuppressibleLoggingHelper? log, ModuleDefinition module, TypeDefinition typeDefinition, ModuleDefinition annotatedModuleDefinition, Dictionary<string, TypeDefinition> attributesOfInterest)
internal static void AnnotateType(SuppressibleLoggingHelper? log, ModuleDefinition module, TypeDefinition typeDefinition, ModuleDefinition annotatedModuleDefinition, Dictionary<string, TypeDefinition> attributesOfInterest)
{
if (attributesOfInterest.ContainsKey(typeDefinition.FullName))
return;
Expand Down Expand Up @@ -174,7 +174,7 @@ private static void AnnotateField(ModuleDefinition module, FieldDefinition field
Annotate(module, fieldDefinition, annotatedFieldDefinition, attributesOfInterest);
}

private static void Annotate(ModuleDefinition module, ICustomAttributeProvider provider, ICustomAttributeProvider annotatedProvider, Dictionary<string, TypeDefinition> attributesOfInterest)
internal static void Annotate(ModuleDefinition module, ICustomAttributeProvider provider, ICustomAttributeProvider annotatedProvider, Dictionary<string, TypeDefinition> attributesOfInterest)
{
// Start by removing excluded attributes as well as attributes of interest to allow, say, rewriting
// netcoreapp3.1 to use .NET 5 reference annotations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("CecilBasedAnnotator, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9a0f527c8e6c74b50a18e7460592e6292488891676d935bf828712a126857ef2e8882e4ff7f9576b9f3b51d5f248b0fedda28f244f92f795beeee9ef04382c0b305da94ab04ed32831815ea27dc475bf4427f059f7d10cc4a025d0f30f5e2a07eadf1e205a1d92788c2e7dee5bc19dba7aa1b9863205d1784e164d346ea89be")]
[assembly: InternalsVisibleTo("ICSharpCode.ReferenceAssemblyAnnotator.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9a0f527c8e6c74b50a18e7460592e6292488891676d935bf828712a126857ef2e8882e4ff7f9576b9f3b51d5f248b0fedda28f244f92f795beeee9ef04382c0b305da94ab04ed32831815ea27dc475bf4427f059f7d10cc4a025d0f30f5e2a07eadf1e205a1d92788c2e7dee5bc19dba7aa1b9863205d1784e164d346ea89be")]
Loading
Loading