diff --git a/src/Nullean.PublicApi.Diff/ApiComparison.cs b/src/Nullean.PublicApi.Diff/ApiComparison.cs index db0fb17..d964851 100644 --- a/src/Nullean.PublicApi.Diff/ApiComparison.cs +++ b/src/Nullean.PublicApi.Diff/ApiComparison.cs @@ -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): diff --git a/src/Nullean.PublicApi/ApiReader.cs b/src/Nullean.PublicApi/ApiReader.cs index ba40a5f..0821577 100644 --- a/src/Nullean.PublicApi/ApiReader.cs +++ b/src/Nullean.PublicApi/ApiReader.cs @@ -65,7 +65,47 @@ public sealed class ApiReader private ApiReader(MetadataReader reader) { _reader = reader; - _types = new TypeReferenceProvider(reader); + _types = new TypeReferenceProvider(reader, NumericIntPtr(reader)); + } + + /// + /// 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. + /// + 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; + } + + /// .NETCoreApp,Version=v8.0 and the like. + 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 } /// Reads the assembly at . diff --git a/src/Nullean.PublicApi/Signatures.cs b/src/Nullean.PublicApi/Signatures.cs index 9648c3c..757eceb 100644 --- a/src/Nullean.PublicApi/Signatures.cs +++ b/src/Nullean.PublicApi/Signatures.cs @@ -18,13 +18,23 @@ internal readonly record struct TypeContext(ImmutableArray TypeParameter } /// Decodes signatures and attribute blobs into s. +/// The assembly's metadata. +/// +/// 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. +/// internal sealed class TypeReferenceProvider( - MetadataReader reader + MetadataReader reader, + bool numericIntPtr = false ) : ISignatureTypeProvider, ICustomAttributeTypeProvider { private readonly Dictionary _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(); diff --git a/tests/Nullean.PublicApi.Tests/.snapshots/WriterTests/Writes.signatures.eee471f4.verified.txt b/tests/Nullean.PublicApi.Tests/.snapshots/WriterTests/Writes.signatures.34d2702d.verified.txt similarity index 93% rename from tests/Nullean.PublicApi.Tests/.snapshots/WriterTests/Writes.signatures.eee471f4.verified.txt rename to tests/Nullean.PublicApi.Tests/.snapshots/WriterTests/Writes.signatures.34d2702d.verified.txt index 12cbf7e..185c632 100644 --- a/tests/Nullean.PublicApi.Tests/.snapshots/WriterTests/Writes.signatures.eee471f4.verified.txt +++ b/tests/Nullean.PublicApi.Tests/.snapshots/WriterTests/Writes.signatures.34d2702d.verified.txt @@ -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 @@ -14,7 +14,7 @@ namespace Lib override bool Equals(object? obj) T Generic(U value) where T : struct, IComparable 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 value) where T : notnull void Nullability(string? a, List b, Dictionary?> c, string?[]? d, int[][,] jagged) diff --git a/tests/Nullean.PublicApi.Tests/ComparisonTests.cs b/tests/Nullean.PublicApi.Tests/ComparisonTests.cs index 156b192..71031ca 100644 --- a/tests/Nullean.PublicApi.Tests/ComparisonTests.cs +++ b/tests/Nullean.PublicApi.Tests/ComparisonTests.cs @@ -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() { diff --git a/tests/Nullean.PublicApi.Tests/WriterTests.cs b/tests/Nullean.PublicApi.Tests/WriterTests.cs index ec64893..b037abf 100644 --- a/tests/Nullean.PublicApi.Tests/WriterTests.cs +++ b/tests/Nullean.PublicApi.Tests/WriterTests.cs @@ -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 {