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
15 changes: 9 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,41 +193,44 @@ jobs:

- name: Smoke test - package quiet
run: |
dotnet-inspect package System.Text.Json 10.0.0 -v:q > smoke-package.md
# Pipe through cat to work around a .NET 11 preview SDK regression
# where redirecting an AOT binary's stdout directly to a file
# ('> file') truncates the output to ~256 bytes. Piping is fine.
dotnet-inspect package System.Text.Json 10.0.0 -v:q | cat > smoke-package.md
cat smoke-package.md
grep -q "System.Text.Json" smoke-package.md
grep -q "Library" smoke-package.md

- name: Smoke test - package normal
run: |
dotnet-inspect package System.Text.Json 10.0.0 -v:n > smoke-package-normal.md
dotnet-inspect package System.Text.Json 10.0.0 -v:n | cat > smoke-package-normal.md
cat smoke-package-normal.md
grep -q "## Package" smoke-package-normal.md
grep -q "## Package Dependencies" smoke-package-normal.md
grep -q "Library" smoke-package-normal.md

- name: Smoke test - type command (oneline default)
run: |
dotnet-inspect type --package [email protected] -t 5 > smoke-type.txt
dotnet-inspect type --package [email protected] -t 5 | cat > smoke-type.txt
cat smoke-type.txt
grep -q "JsonSerializer" smoke-type.txt

- name: Smoke test - member command (oneline default)
run: |
dotnet-inspect member JsonSerializer --package [email protected] -m 5 > smoke-member.txt
dotnet-inspect member JsonSerializer --package [email protected] -m 5 | cat > smoke-member.txt
cat smoke-member.txt
grep -q "Serialize" smoke-member.txt
grep -q "Deserialize" smoke-member.txt

- name: Smoke test - type command (markdown)
run: |
dotnet-inspect type --package [email protected] -v:m -t 5 > smoke-type.md
dotnet-inspect type --package [email protected] -v:m -t 5 | cat > smoke-type.md
cat smoke-type.md
grep -q "JsonSerializer" smoke-type.md

- name: Smoke test - member command (markdown)
run: |
dotnet-inspect member JsonSerializer --package [email protected] -v:m -m 5 > smoke-member.md
dotnet-inspect member JsonSerializer --package [email protected] -v:m -m 5 | cat > smoke-member.md
cat smoke-member.md
grep -q "JsonSerializer" smoke-member.md
grep -q "Serialize" smoke-member.md
Expand Down
4 changes: 2 additions & 2 deletions docs/lap-around.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ Name: System.Text.Json | Version: 10.0.3 | TFM: .NETCoreApp,Version=v10.0 | Arch
| Company | Microsoft Corporation |
| Copyright | © Microsoft Corporation. All rights reserved. |
| Signed | Yes |
| Public Key Token | 1d05d9bed22b38cb |
| Public Key Token | cc7b13ffcd2ddd51 |
| Deterministic | ✓ |
| Reproducible | ✓ |
| File Size | 80.8 KB |
Expand Down Expand Up @@ -543,7 +543,7 @@ Name: System.Text.Json | Version: 10.0.3 | TFM: .NETCoreApp,Version=v10.0 | Arch
| Company | Microsoft Corporation |
| Copyright | © Microsoft Corporation. All rights reserved. |
| Signed | Yes |
| Public Key Token | 1d05d9bed22b38cb |
| Public Key Token | cc7b13ffcd2ddd51 |
| Deterministic | ✓ |
| Reproducible | ✓ |
| File Size | 80.8 KB |
Expand Down
4 changes: 2 additions & 2 deletions scripts/baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ COMMAND: assembly --package [email protected] --tfm net10.0
| Compilation | CoreCLR |
| Informational Version | 10.0.2+44525024595742ebe09023abe709df51de65009b |
| Signed | Yes |
| Public Key Token | 1d05d9bed22b38cb |
| Public Key Token | cc7b13ffcd2ddd51 |

================================================================================
COMMAND: assembly --package [email protected] --tfm net10.0 --audit
Expand All @@ -775,7 +775,7 @@ COMMAND: assembly --package [email protected] --tfm net10.0 --audit
| Compilation | CoreCLR |
| Informational Version | 10.0.2+44525024595742ebe09023abe709df51de65009b |
| Signed | Yes |
| Public Key Token | 1d05d9bed22b38cb |
| Public Key Token | cc7b13ffcd2ddd51 |

## Build Audit

Expand Down
9 changes: 6 additions & 3 deletions src/DotnetInspector.Metadata/AssemblyInspector.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;

namespace DotnetInspector.Metadata;

Expand Down Expand Up @@ -83,7 +84,10 @@ public static AssemblyInfo ExtractAssemblyInfo(PEReader peReader, bool includeRe
var publicKey = metadataReader.GetBlobBytes(assemblyDef.PublicKey);
if (publicKey.Length > 0)
{
info.PublicKeyToken = Convert.ToHexString(publicKey.TakeLast(8).ToArray()).ToLowerInvariant();
var publicKeyHash = SHA1.HashData(publicKey);
var publicKeyToken = publicKeyHash[^8..];
Array.Reverse(publicKeyToken);
info.PublicKeyToken = Convert.ToHexString(publicKeyToken).ToLowerInvariant();
}
}

Expand Down Expand Up @@ -429,8 +433,7 @@ private static (bool isNormalized, List<string> nonNormalizedPaths) CheckSourceL
string url = prop.Value.GetString() ?? "";
if (url.Contains("github.com", StringComparison.OrdinalIgnoreCase))
{
var match = System.Text.RegularExpressions.Regex.Match(url,
@"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/");
var match = AssemblyInspectorRegex.GitHubRawUrl().Match(url);
if (match.Success)
{
return $"https://github.com/{match.Groups[1].Value}/{match.Groups[2].Value}";
Expand Down
9 changes: 9 additions & 0 deletions src/DotnetInspector.Metadata/AssemblyInspectorRegex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.RegularExpressions;

namespace DotnetInspector.Metadata;

internal static partial class AssemblyInspectorRegex
{
[GeneratedRegex(@"https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/")]
internal static partial Regex GitHubRawUrl();
}
32 changes: 32 additions & 0 deletions tests/DotnetInspector.Metadata.Tests/AssemblyInspectorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using DotnetInspector.Metadata;

namespace DotnetInspector.Metadata.Tests;

public sealed class AssemblyInspectorTests
{
public static IEnumerable<object[]> StrongNamedAssemblies()
{
yield return [typeof(System.Text.Json.JsonSerializer).Assembly];
yield return [typeof(object).Assembly];
yield return [typeof(System.Linq.Enumerable).Assembly];
}

[Theory]
[MemberData(nameof(StrongNamedAssemblies))]
public void ExtractAssemblyInfo_PublicKeyToken_MatchesRuntimeGroundTruth(Assembly assembly)
{
var expectedTokenBytes = Assert.IsType<byte[]>(AssemblyName.GetAssemblyName(assembly.Location).GetPublicKeyToken());
Assert.NotEmpty(expectedTokenBytes);

using var stream = File.OpenRead(assembly.Location);
using var peReader = new PEReader(stream);

var info = AssemblyInspector.ExtractAssemblyInfo(peReader);

var expectedToken = Convert.ToHexString(expectedTokenBytes).ToLowerInvariant();
Assert.Equal(expectedToken, info.PublicKeyToken);
}
}
42 changes: 27 additions & 15 deletions tests/DotnetInspector.Metadata.Tests/NullabilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace DotnetInspector.Metadata.Tests;
/// Tests for nullability annotation reading and rendering in API signatures.
/// Uses the test assembly itself (compiled with nullable enabled) as the test subject.
/// </summary>
public class NullabilityTests
public sealed class NullabilityTests
{
private static readonly ApiSurface Surface;

Expand Down Expand Up @@ -39,37 +39,48 @@ private static ApiMember GetField(string typeName, string fieldName) =>
[Fact]
public void GetNullableContext_ReturnsDefault_WhenNotPresent()
{
// Value types typically don't have NullableContextAttribute
var reader = GetMetadataReader();
// NullableObliviousClass is defined below in a #nullable disable region,
// so it has no NullableContextAttribute in this assembly.
// PEReader/stream are inlined (not shared) because MetadataReader borrows memory
// owned by PEReader — sharing a static reader would leak the file handle.
var assemblyPath = typeof(NullabilityTests).Assembly.Location;
using var stream = File.OpenRead(assemblyPath);
using var peReader = new PEReader(stream);
var reader = peReader.GetMetadataReader();

foreach (var typeHandle in reader.TypeDefinitions)
{
var typeDef = reader.GetTypeDefinition(typeHandle);
if (reader.GetString(typeDef.Name) == "Int32")
if (reader.GetString(typeDef.Name) == nameof(NullableObliviousClass))
{
// System.Int32 doesn't have a NullableContext attribute
var context = NullabilityReader.GetNullableContext(reader, typeDef.GetCustomAttributes());
Assert.Equal(0, context);
return;
}
}
Assert.Fail($"{nameof(NullableObliviousClass)} type definition not found in test assembly");
}

[Fact]
public void GetNullableContext_ReadsValue_WhenPresent()
{
// This test assembly has nullable=enable, so types should have NullableContextAttribute
var reader = GetMetadataReader();
// See comment in GetNullableContext_ReturnsDefault_WhenNotPresent for why PEReader is inlined.
var assemblyPath = typeof(NullabilityTests).Assembly.Location;
using var stream = File.OpenRead(assemblyPath);
using var peReader = new PEReader(stream);
var reader = peReader.GetMetadataReader();

foreach (var typeHandle in reader.TypeDefinitions)
{
var typeDef = reader.GetTypeDefinition(typeHandle);
if (reader.GetString(typeDef.Name) == nameof(NullableSampleClass))
{
var context = NullabilityReader.GetNullableContext(reader, typeDef.GetCustomAttributes());
// Should be 1 (not annotated) or 2 (annotated) — non-zero for nullable-enabled types
Assert.NotEqual(0, context);
return;
}
}
Assert.Fail($"{nameof(NullableSampleClass)} type definition not found in test assembly");
}

// --- TypeNode tree + rendering tests ---
Expand Down Expand Up @@ -250,13 +261,6 @@ public void Method_GenericNullableArg()
Assert.Contains("string?", member.Signature);
}

private static MetadataReader GetMetadataReader()
{
var path = typeof(NullabilityTests).Assembly.Location;
var stream = File.OpenRead(path);
var peReader = new PEReader(stream);
return peReader.GetMetadataReader();
}
}

// ===== Test fixture types compiled with #nullable enable =====
Expand All @@ -281,3 +285,11 @@ public void TakesNonNullableList(List<string> items) { }

public Dictionary<string, object?> MixedGeneric() => new();
}

#nullable disable
// Fixture type with no NullableContextAttribute — used to test GetNullableContext returns 0
public class NullableObliviousClass
{
public string Value;
}
#nullable restore
Loading