Skip to content
Open
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dotnet build ReferenceAssemblyAnnotator.slnx -c Release

## Tests

`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:
`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`, and `ProgramMainTests` runs `Program.Main` end to end on netstandard2.0's `netstandard.dll` (from a `NETStandard.Library` download) against the net10.0 one the test project compiles against. The project 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
Expand All @@ -42,7 +42,7 @@ dotnet msbuild -restore tests/SingleTFM -p:TestFramework=netstandard2.0 -warnase
```

- `tests/SingleTFM` is parameterized by `-p:TestFramework=` (net35 … net10.0); MultiTFM, RefVersion3.1.0, NoDisabledWarnings (uses `-p:TreatWarningsAsErrors=true`, since `-warnaserror` ignores `WarningsNotAsErrors`) and WPF cover the rest.
- 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`).
- 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`). The `Task.IsFaulted` line checks that `MemberNotNullWhen` is copied (CS8602 otherwise); it is left out of `RefVersion3.1.0` and excluded for net35 and netcoreapp3.x, whose references lack the attribute.
- 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, 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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,25 @@
<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" />
<!-- Unannotated input for ProgramMainTests -->
<PackageDownload Include="NETStandard.Library" Version="[2.0.3]" />
</ItemGroup>

<!-- ProgramMainTests annotates netstandard2.0's netstandard.dll with the net10.0 one this project compiles against. -->
<Target Name="AddProgramMainTestsPaths" BeforeTargets="GetAssemblyAttributes" DependsOnTargets="ResolveTargetingPackAssets">
<ItemGroup>
<_AnnotatedNetStandard Include="@(Reference)" Condition="'%(Filename)' == 'netstandard'" />
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>UnannotatedNetStandard</_Parameter1>
<_Parameter2>$([MSBuild]::EnsureTrailingSlash($(NuGetPackageRoot)))netstandard.library/2.0.3/build/netstandard2.0/ref/netstandard.dll</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>AnnotatedNetStandard</_Parameter1>
<_Parameter2>@(_AnnotatedNetStandard->'%(FullPath)')</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>

<ItemGroup>
<ProjectReference Include="..\ICSharpCode.ReferenceAssemblyAnnotator\ICSharpCode.ReferenceAssemblyAnnotator.csproj" />
</ItemGroup>
Expand Down
104 changes: 104 additions & 0 deletions ICSharpCode.ReferenceAssemblyAnnotator.Tests/ProgramMainTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
using Xunit;

/// <summary>
/// Runs <see cref="Program.Main"/> end to end: annotates netstandard2.0's netstandard.dll and reads the output back.
/// </summary>
public class ProgramMainTests
{
private const string CodeAnalysis = "System.Diagnostics.CodeAnalysis";

private static readonly Lazy<ModuleDefinition> Annotated = new Lazy<ModuleDefinition>(AnnotateNetStandard);

[Fact]
public void CopiesMemberNotNullWhen()
{
var isFaulted = GetType("System.Threading.Tasks", "Task").Properties.Single(property => property.Name == "IsFaulted");

var attribute = GetAttribute(isFaulted, "MemberNotNullWhenAttribute");

Assert.Equal(new object[] { true, "Exception" }, attribute.ConstructorArguments.Select(argument => argument.Value));
}

[Fact]
public void CopiesDynamicallyAccessedMembersWithEnumArgument()
{
var getMethods = GetType("System", "Type").Methods.Single(method => method.Name == "GetMethods" && method.Parameters.Count == 1);

var attribute = GetAttribute(getMethods, "DynamicallyAccessedMembersAttribute");

// PublicMethods | NonPublicMethods
Assert.Equal(8 | 16, (int)attribute.ConstructorArguments.Single().Value);
}

[Theory]
[InlineData("System.Reflection", "Assembly", "GetTypes", "RequiresUnreferencedCodeAttribute")]
[InlineData("System", "Type", "MakeGenericType", "RequiresDynamicCodeAttribute")]
[InlineData("System.Reflection", "Assembly", "GetFiles", "RequiresAssemblyFilesAttribute")]
public void CopiesRequiresAttributes(string @namespace, string typeName, string methodName, string attributeName)
{
var method = GetType(@namespace, typeName).Methods.First(method => method.Name == methodName);

Assert.NotEmpty(GetAttribute(method, attributeName).ConstructorArguments);
}

[Fact]
public void AssemblyResolver_LoadsDependenciesWithItself()
{
string netStandard = GetMetadata("UnannotatedNetStandard");
using var resolver = new AssemblyResolver(ImmutableArray.Create(Path.GetDirectoryName(netStandard)!));

// The System.Runtime facade forwards Object to netstandard, which must not come from the test host's runtime.
var facade = resolver.Resolve(AssemblyNameReference.Parse("System.Runtime"))!;

Assert.Equal(Path.GetFullPath(netStandard), facade.MainModule.TypeSystem.Object.Resolve().Module.FileName, ignoreCase: true);
}

[Theory]
[InlineData("MemberNotNullAttribute")]
[InlineData("MemberNotNullWhenAttribute")]
[InlineData("RequiresUnreferencedCodeAttribute")]
[InlineData("RequiresDynamicCodeAttribute")]
[InlineData("RequiresAssemblyFilesAttribute")]
[InlineData("DynamicallyAccessedMembersAttribute")]
[InlineData("DynamicallyAccessedMemberTypes")]
public void SynthesizesInternalTypes(string typeName)
{
Assert.False(GetType(CodeAnalysis, typeName).IsPublic);
}

private static TypeDefinition GetType(string @namespace, string name)
=> Annotated.Value.GetType(@namespace, name) ?? throw new InvalidOperationException($"{@namespace}.{name} not found");

private static CustomAttribute GetAttribute(Mono.Cecil.ICustomAttributeProvider provider, string name)
=> provider.CustomAttributes.Single(attribute => attribute.AttributeType.FullName == $"{CodeAnalysis}.{name}");

private static string GetMetadata(string key)
=> typeof(ProgramMainTests).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().Single(attribute => attribute.Key == key).Value!;

private static ModuleDefinition AnnotateNetStandard()
{
string input = GetMetadata("UnannotatedNetStandard");
string output = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".dll");
try
{
Program.Main(null, input, ImmutableArray.Create(Path.GetDirectoryName(input)!), GetMetadata("AnnotatedNetStandard"), output);
return ModuleDefinition.ReadModule(new MemoryStream(File.ReadAllBytes(output)));
}
finally
{
File.Delete(output);
}
}
}
}
8 changes: 5 additions & 3 deletions ICSharpCode.ReferenceAssemblyAnnotator/AssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ namespace ICSharpCode.ReferenceAssemblyAnnotator

internal sealed class AssemblyResolver : IAssemblyResolver
{
private static readonly ReaderParameters DefaultReaderParameters = new ReaderParameters(ReadingMode.Deferred);

private readonly ImmutableArray<string> _searchDirectories;
private readonly Dictionary<string, AssemblyDefinition?> _assembliesByFileName = new Dictionary<string, AssemblyDefinition?>();

// Dependencies must resolve through this resolver too; Cecil's default one would find the build host's runtime.
private readonly ReaderParameters _readerParameters;

public AssemblyResolver(ImmutableArray<string> searchDirectories)
{
_searchDirectories = searchDirectories;
_readerParameters = new ReaderParameters(ReadingMode.Deferred) { AssemblyResolver = this };
}

public AssemblyDefinition? Resolve(AssemblyNameReference name)
{
return Resolve(name, DefaultReaderParameters);
return Resolve(name, _readerParameters);
}

public AssemblyDefinition? Resolve(AssemblyNameReference name, ReaderParameters parameters)
Expand Down
8 changes: 7 additions & 1 deletion ICSharpCode.ReferenceAssemblyAnnotator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ internal static void Main(SuppressibleLoggingHelper? log, string referenceAssemb
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisNotNullAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisNotNullIfNotNullAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisNotNullWhenAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisMemberNotNullAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisMemberNotNullWhenAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisRequiresUnreferencedCodeAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisRequiresDynamicCodeAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisRequiresAssemblyFilesAttribute);
AddAttributeOfInterest(attributesOfInterest, wellKnownTypes.SystemDiagnosticsCodeAnalysisDynamicallyAccessedMembersAttribute);

AnnotateAssembly(log, assemblyDefinition, annotatedAssemblyDefinition, attributesOfInterest);

Expand Down Expand Up @@ -216,7 +222,7 @@ internal static void Annotate(ModuleDefinition module, ICustomAttributeProvider

static bool IsMatchingConstructor(MethodDefinition constructor, CustomAttribute customAttribute)
{
if (constructor.Parameters.Count != customAttribute.ConstructorArguments.Count)
if (!constructor.IsConstructor || constructor.Parameters.Count != customAttribute.ConstructorArguments.Count)
return false;

for (int i = 0; i < constructor.Parameters.Count; i++)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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
{
using Mono.Cecil;

internal partial class WellKnownTypes
{
// Only the underlying type matters: attribute blobs store the enum value as int32.
private sealed class DynamicallyAccessedMemberTypesProvidedType : ProvidedType
{
public DynamicallyAccessedMemberTypesProvidedType()
: base("System.Diagnostics.CodeAnalysis", "DynamicallyAccessedMemberTypes")
{
}

protected override TypeReference DefineType(ModuleDefinition module, WellKnownTypes wellKnownTypes, CustomAttributeFactory attributeFactory)
{
var type = new TypeDefinition(
@namespace: NamespaceName,
name: TypeName,
TypeAttributes.NotPublic | TypeAttributes.Sealed,
module.ImportReference(wellKnownTypes.SystemEnum));

type.Fields.Add(new FieldDefinition(
name: "value__",
attributes: FieldAttributes.Public | FieldAttributes.SpecialName | FieldAttributes.RTSpecialName,
fieldType: module.TypeSystem.Int32));

module.Types.Add(type);

return type;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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
{
using System;
using Mono.Cecil;

internal partial class WellKnownTypes
{
private sealed class DynamicallyAccessedMembersAttributeProvidedType : ProvidedAttributeType
{
public DynamicallyAccessedMembersAttributeProvidedType()
: base("System.Diagnostics.CodeAnalysis", "DynamicallyAccessedMembersAttribute")
{
}

protected override void ImplementAttribute(ModuleDefinition module, TypeDefinition attribute, WellKnownTypes wellKnownTypes, CustomAttributeFactory attributeFactory)
{
attribute.CustomAttributes.Add(attributeFactory.AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, inherited: false));

var constructor = MethodFactory.Constructor(wellKnownTypes.TypeSystem);
constructor.Parameters.Add(new ParameterDefinition("memberTypes", ParameterAttributes.None, wellKnownTypes.SystemDiagnosticsCodeAnalysisDynamicallyAccessedMemberTypes.Value));
attribute.Methods.Add(constructor);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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
{
using System;
using Mono.Cecil;
using Mono.Cecil.Rocks;

internal partial class WellKnownTypes
{
private sealed class MemberNotNullAttributeProvidedType : ProvidedAttributeType
{
public MemberNotNullAttributeProvidedType()
: base("System.Diagnostics.CodeAnalysis", "MemberNotNullAttribute")
{
}

protected override void ImplementAttribute(ModuleDefinition module, TypeDefinition attribute, WellKnownTypes wellKnownTypes, CustomAttributeFactory attributeFactory)
{
attribute.CustomAttributes.Add(attributeFactory.NullableContext(1));
attribute.CustomAttributes.Add(attributeFactory.Nullable(0));
attribute.CustomAttributes.Add(attributeFactory.AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, allowMultiple: true, inherited: false));

var constructor1 = MethodFactory.Constructor(wellKnownTypes.TypeSystem);
constructor1.Parameters.Add(new ParameterDefinition("member", ParameterAttributes.None, wellKnownTypes.TypeSystem.String));
attribute.Methods.Add(constructor1);

var constructor2 = MethodFactory.Constructor(wellKnownTypes.TypeSystem);
constructor2.Parameters.Add(new ParameterDefinition("members", ParameterAttributes.None, wellKnownTypes.TypeSystem.String.MakeArrayType()));
attribute.Methods.Add(constructor2);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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
{
using System;
using Mono.Cecil;
using Mono.Cecil.Rocks;

internal partial class WellKnownTypes
{
private sealed class MemberNotNullWhenAttributeProvidedType : ProvidedAttributeType
{
public MemberNotNullWhenAttributeProvidedType()
: base("System.Diagnostics.CodeAnalysis", "MemberNotNullWhenAttribute")
{
}

protected override void ImplementAttribute(ModuleDefinition module, TypeDefinition attribute, WellKnownTypes wellKnownTypes, CustomAttributeFactory attributeFactory)
{
attribute.CustomAttributes.Add(attributeFactory.NullableContext(1));
attribute.CustomAttributes.Add(attributeFactory.Nullable(0));
attribute.CustomAttributes.Add(attributeFactory.AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, allowMultiple: true, inherited: false));

var constructor1 = MethodFactory.Constructor(wellKnownTypes.TypeSystem);
constructor1.Parameters.Add(new ParameterDefinition("returnValue", ParameterAttributes.None, wellKnownTypes.TypeSystem.Boolean));
constructor1.Parameters.Add(new ParameterDefinition("member", ParameterAttributes.None, wellKnownTypes.TypeSystem.String));
attribute.Methods.Add(constructor1);

var constructor2 = MethodFactory.Constructor(wellKnownTypes.TypeSystem);
constructor2.Parameters.Add(new ParameterDefinition("returnValue", ParameterAttributes.None, wellKnownTypes.TypeSystem.Boolean));
constructor2.Parameters.Add(new ParameterDefinition("members", ParameterAttributes.None, wellKnownTypes.TypeSystem.String.MakeArrayType()));
attribute.Methods.Add(constructor2);
}
}
}
}
Loading
Loading