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
6 changes: 6 additions & 0 deletions src/Nullean.PublicApi.Diff/ApiComparison.cs
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,12 @@ private static bool Same(ApiTypeReference a, ApiTypeReference b, Sameness samene
a = Runtime(a);
b = Runtime(b);
}
else
{
// nint and IntPtr are one type to C# as well, since C# 11: only how a file spelled it differs
a = a is ApiNativeIntegerType ? Runtime(a) with { IsNullable = a.IsNullable } : a;
b = b is ApiNativeIntegerType ? Runtime(b) with { IsNullable = b.IsNullable } : b;
}
switch (a, b)
{
case (ApiNamedType x, ApiNamedType y):
Expand Down
42 changes: 41 additions & 1 deletion src/Nullean.PublicApi/ApiReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,47 @@ public sealed class ApiReader
private ApiReader(MetadataReader reader)
{
_reader = reader;
_types = new TypeReferenceProvider(reader);
_types = new TypeReferenceProvider(reader, NumericIntPtr(reader));
}

/// <summary>
/// Whether the assembly was built for a runtime where IntPtr is nint: one targeting .NET 7 or later, or a core
/// library that declares RuntimeFeature.NumericIntPtr itself.
/// </summary>
private static bool NumericIntPtr(MetadataReader reader)
{
foreach (var handle in reader.TypeDefinitions)
{
var type = reader.GetTypeDefinition(handle);
if (reader.StringComparer.Equals(type.Name, "RuntimeFeature") && reader.StringComparer.Equals(type.Namespace, "System.Runtime.CompilerServices"))
return type.GetFields().Any(f => reader.StringComparer.Equals(reader.GetFieldDefinition(f).Name, "NumericIntPtr"));
}
var probe = new TypeReferenceProvider(reader);
foreach (var handle in reader.GetAssemblyDefinition().GetCustomAttributes())
{
var attribute = reader.GetCustomAttribute(handle);
if (attribute.Constructor.Kind != HandleKind.MemberReference)
continue;
var parent = reader.GetMemberReference((MemberReferenceHandle)attribute.Constructor).Parent;
if (parent.Kind != HandleKind.TypeReference || probe.FullName(parent) != "System.Runtime.Versioning.TargetFrameworkAttribute")
continue;
var value = attribute.DecodeValue(probe);
if (value.FixedArguments is [{ Value: string framework }, ..])
return IsNet7OrLater(framework);
}
return false;
}

/// <summary><c>.NETCoreApp,Version=v8.0</c> and the like.</summary>
private static bool IsNet7OrLater(string framework)
{
const string prefix = ".NETCoreApp,Version=v";
#pragma warning disable CA1846 // netstandard2.0 cannot parse a span
return framework.StartsWith(prefix, StringComparison.Ordinal) && Version.TryParse(
framework.Substring(prefix.Length),
out var version
) && version.Major >= 7;
#pragma warning restore CA1846
}

/// <summary>Reads the assembly at <paramref name="path"/>.</summary>
Expand Down
14 changes: 12 additions & 2 deletions src/Nullean.PublicApi/Signatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ internal readonly record struct TypeContext(ImmutableArray<string> TypeParameter
}

/// <summary>Decodes signatures and attribute blobs into <see cref="ApiTypeReference"/>s.</summary>
/// <param name="reader">The assembly's metadata.</param>
/// <param name="numericIntPtr">
/// Whether IntPtr is nint: since C# 11, on a runtime that says so (.NET 7 and later), they are the same type and the
/// compiler no longer marks which one a signature meant.
/// </param>
internal sealed class TypeReferenceProvider(
MetadataReader reader
MetadataReader reader,
bool numericIntPtr = false
) : ISignatureTypeProvider<ApiTypeReference, TypeContext>, ICustomAttributeTypeProvider<ApiTypeReference>
{
private readonly Dictionary<EntityHandle, ApiNamedType> _named = [];

public ApiTypeReference GetPrimitiveType(PrimitiveTypeCode typeCode) => Primitive(typeCode);
private static readonly ApiNativeIntegerType Nint = new(false);
private static readonly ApiNativeIntegerType Nuint = new(true);

public ApiTypeReference GetPrimitiveType(PrimitiveTypeCode typeCode) =>
numericIntPtr && typeCode is PrimitiveTypeCode.IntPtr ? Nint : numericIntPtr && typeCode is PrimitiveTypeCode.UIntPtr ? Nuint : Primitive(typeCode);

// the records are immutable, so every reference to int or string shares one, made once
private static readonly ApiNamedType?[] Primitives = BuildPrimitives();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
assembly Lib
using System: IComparable<>
using struct System: DateTime, DayOfWeek, IntPtr, UIntPtr
using struct System: DateTime, DayOfWeek
using System.Collections.Generic: Dictionary<,>, List<>

namespace Lib
Expand All @@ -14,7 +14,7 @@ namespace Lib
override bool Equals(object? obj)
T Generic<T, U>(U value) where T : struct, IComparable<T> where U : class?, new()
override int GetHashCode()
IntPtr Native(UIntPtr value)
nint Native(nuint value)
List<(int a, (string b, int c) inner)> NestedTuples()
void NotNull<T>(T value) where T : notnull
void Nullability(string? a, List<string?> b, Dictionary<string, List<int?>?> c, string?[]? d, int[][,] jagged)
Expand Down
8 changes: 8 additions & 0 deletions tests/Nullean.PublicApi.Tests/ComparisonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ public void Configured_not_an_extension_point_does_not_count()
Report(result).Should().MatchSnapshot();
}

[Test]
public void IntPtr_read_as_nint_is_no_change()
{
const string source = "public class A { public System.IntPtr M(System.UIntPtr value) => default; }";
var result = Compare(source, "[assembly: System.Runtime.Versioning.TargetFramework(\".NETCoreApp,Version=v11.0\")]\n" + source);
result.Changes.Should().BeEmpty();
}

[Test]
public void A_break_below_one_asks_for_a_minor_version()
{
Expand Down
2 changes: 2 additions & 0 deletions tests/Nullean.PublicApi.Tests/WriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ protected class Family { }
yield return ("signatures", """
using System;
using System.Collections.Generic;
// built for .NET 11, where IntPtr is nint, as the SDK says in every build
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v11.0")]
namespace Lib;
public class Signatures
{
Expand Down
Loading