Add authoritative public API catalog
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
/target/
|
/target/
|
||||||
|
/tools/api-catalog/bin/
|
||||||
|
/tools/api-catalog/obj/
|
||||||
|
|||||||
1
AGENTS.md
Normal file
1
AGENTS.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
All code and API mappings must be cross-platform; do not use macOS-only APIs unless equivalent Linux and Windows implementations are provided.
|
||||||
@@ -14,6 +14,11 @@ complete public API shim.
|
|||||||
The source snapshot, compatibility rules, dependency research, and ordered
|
The source snapshot, compatibility rules, dependency research, and ordered
|
||||||
implementation plan are in [RUSTREWRITE.md](RUSTREWRITE.md).
|
implementation plan are in [RUSTREWRITE.md](RUSTREWRITE.md).
|
||||||
|
|
||||||
|
The authoritative compiled .NET surface is checked in as
|
||||||
|
[`api/public-api.json`](api/public-api.json); [`api/README.md`](api/README.md)
|
||||||
|
documents deterministic regeneration and the external types that must be
|
||||||
|
mapped to native Rust equivalents.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo build --workspace
|
cargo build --workspace
|
||||||
cargo test --workspace --no-run
|
cargo test --workspace --no-run
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ Generated inventories are intentionally checked in:
|
|||||||
crate/module/type and source line.
|
crate/module/type and source line.
|
||||||
- `api/SURFACE.tsv` records every public declaration line in the 13 library
|
- `api/SURFACE.tsv` records every public declaration line in the 13 library
|
||||||
projects, including a SHA-256 digest.
|
projects, including a SHA-256 digest.
|
||||||
|
- `api/public-api.json` is the authoritative compiled-metadata catalog for all
|
||||||
|
13 library assemblies after all six source generators run. It also records
|
||||||
|
every external type referenced by a public signature as a pending Rust
|
||||||
|
mapping obligation.
|
||||||
- `tests/upstream-tests.json` records every NUnit invocation and the SHA-256 of
|
- `tests/upstream-tests.json` records every NUnit invocation and the SHA-256 of
|
||||||
its C# method body.
|
its C# method body.
|
||||||
- `tests/PARITY.md` maps every NUnit invocation to its Rust test name.
|
- `tests/PARITY.md` maps every NUnit invocation to its Rust test name.
|
||||||
@@ -27,9 +31,11 @@ Generated inventories are intentionally checked in:
|
|||||||
nine sample/tool projects.
|
nine sample/tool projects.
|
||||||
|
|
||||||
Run `python3 tools/generate_surface.py` after deliberately changing the pinned
|
Run `python3 tools/generate_surface.py` after deliberately changing the pinned
|
||||||
upstream snapshot. The generator rejects a test count other than 1,295 so an
|
upstream snapshot, then run `python3 tools/extract_public_api.py` to rebuild the
|
||||||
upstream drift cannot silently remove tests. Update the commit constant and
|
authoritative metadata catalog twice and require byte-identical output. The
|
||||||
review all ledger diffs in the same change.
|
source generator rejects a test count other than 1,295 so upstream drift cannot
|
||||||
|
silently remove tests. Update both commit constants and review all ledger diffs
|
||||||
|
in the same change.
|
||||||
|
|
||||||
### Current shell status
|
### Current shell status
|
||||||
|
|
||||||
@@ -38,6 +44,9 @@ The current workspace is a structural baseline, not a working client:
|
|||||||
- all 13 public .NET library projects have corresponding Rust crates;
|
- all 13 public .NET library projects have corresponding Rust crates;
|
||||||
- 1,705 public C# type declarations produce compiling Rust type/trait shims;
|
- 1,705 public C# type declarations produce compiling Rust type/trait shims;
|
||||||
- 12,195 public declaration lines are retained in the API ledger;
|
- 12,195 public declaration lines are retained in the API ledger;
|
||||||
|
- compiled metadata records 3,066 public types and 30,789 public members with
|
||||||
|
matching independent ECMA-335 counts, plus 142 external signature types that
|
||||||
|
still require explicit Rust mapping decisions;
|
||||||
- all 1,295 NUnit `[Test]`/`[TestCase]` invocations have compiling Rust test
|
- all 1,295 NUnit `[Test]`/`[TestCase]` invocations have compiling Rust test
|
||||||
entries that intentionally panic with their source identity and body hash;
|
entries that intentionally panic with their source identity and body hash;
|
||||||
- all nine sample/tool projects have compiling Rust binary targets;
|
- all nine sample/tool projects have compiling Rust binary targets;
|
||||||
@@ -184,12 +193,16 @@ for every public/nested-public:
|
|||||||
- event and delegate payload;
|
- event and delegate payload;
|
||||||
- base type, implemented interface and relevant public attribute.
|
- base type, implemented interface and relevant public attribute.
|
||||||
|
|
||||||
Create `api/public-api.json` as that catalog and `api/RUST-MAPPING.tsv` with one
|
`api/public-api.json` is that catalog. Its `external_types` section records each
|
||||||
row per C# member: stable C# documentation ID, C# signature, Rust crate/path,
|
non-LibreMetaverse type used by a public signature, its defining assembly, and
|
||||||
Rust signature, mapping decision, and status. A coverage checker must fail for
|
every owning LibreMetaverse documentation ID; it intentionally does not copy
|
||||||
missing members, duplicate Rust destinations, unresolved types, or stale source
|
the external assembly's member surface. Create `api/RUST-MAPPING.tsv` with one
|
||||||
IDs. `api/SURFACE.tsv` remains useful for source navigation but is not the
|
row per C# member and resolve every external entry to Rust core/std, an adopted
|
||||||
coverage authority.
|
crate, or a native MetaCrate replacement: stable C# documentation ID, C#
|
||||||
|
signature, Rust crate/path, Rust signature, mapping decision, and status. A
|
||||||
|
coverage checker must fail for missing members, duplicate Rust destinations,
|
||||||
|
unresolved external types, or stale source IDs. `api/SURFACE.tsv` remains useful
|
||||||
|
for source navigation but is not the coverage authority.
|
||||||
|
|
||||||
Each mapped Rust item must actually compile and be callable:
|
Each mapped Rust item must actually compile and be callable:
|
||||||
|
|
||||||
|
|||||||
30
api/README.md
Normal file
30
api/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Public API catalog
|
||||||
|
|
||||||
|
`public-api.json` is the authoritative compiled-metadata inventory for the 13
|
||||||
|
LibreMetaverse library assemblies at the pinned upstream commit. It contains:
|
||||||
|
|
||||||
|
- stable C# documentation IDs and complete public type/member metadata;
|
||||||
|
- nullability, constraints, bases, interfaces, overload parameters, accessors,
|
||||||
|
constants, events, delegates, enum values, and public attributes;
|
||||||
|
- assembly identities and independent reflection/ECMA-335 count checks;
|
||||||
|
- hashes for the outputs of all six upstream source generators;
|
||||||
|
- `external_types`, the non-LibreMetaverse types used by public signatures and
|
||||||
|
every LibreMetaverse API that uses them.
|
||||||
|
|
||||||
|
External entries are replacement obligations, not copied dependency APIs.
|
||||||
|
Issue #3 must map each one to a Rust crate/path: use Rust core/std or an adopted
|
||||||
|
crate where it preserves the contract, otherwise define a native MetaCrate
|
||||||
|
type. The catalog deliberately does not enumerate members of SkiaSharp,
|
||||||
|
SIPSorcery, MessagePack, CoreJ2K, or any other external assembly.
|
||||||
|
|
||||||
|
Regenerate from the clean pinned upstream checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/extract_public_api.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The command builds the upstream libraries into a temporary directory, runs the
|
||||||
|
extractor twice against the same compiled metadata, rejects count mismatches or
|
||||||
|
duplicate documentation IDs, and writes the result only when both outputs are
|
||||||
|
byte-identical. The extractor is development-only and is absent from every
|
||||||
|
Cargo manifest and published Rust dependency graph.
|
||||||
1296068
api/public-api.json
Normal file
1296068
api/public-api.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
//! MeshFoundry renderer corresponding to `LibreMetaverse.Rendering.MeshFoundry`.
|
//! `MeshFoundry` renderer corresponding to `LibreMetaverse.Rendering.MeshFoundry`.
|
||||||
|
|
||||||
mod generated;
|
mod generated;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
//! Shared failure marker used by generated compatibility tests.
|
//! Shared failure marker used by generated compatibility tests.
|
||||||
|
|
||||||
/// Fails a not-yet-translated upstream test while retaining parity metadata.
|
/// Fails a not-yet-translated upstream test while retaining parity metadata.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Always, with the retained upstream test identity.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn pending(source: &str, line: u64, test: &str, attribute: &str, body_sha256: &str) -> ! {
|
pub fn pending(source: &str, line: u64, test: &str, attribute: &str, body_sha256: &str) -> ! {
|
||||||
panic!(
|
panic!(
|
||||||
|
|||||||
9
tools/api-catalog/ApiCatalog.csproj
Normal file
9
tools/api-catalog/ApiCatalog.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
695
tools/api-catalog/Program.cs
Normal file
695
tools/api-catalog/Program.cs
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.Metadata;
|
||||||
|
using System.Reflection.PortableExecutable;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
if (args is ["--self-test"])
|
||||||
|
{
|
||||||
|
ApiCatalog.SelfTest();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Length < 10 || args[0] != "--output" || args[2] != "--upstream-commit" ||
|
||||||
|
args[4] != "--target-framework" || args[6] != "--generated-root")
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"usage: api-catalog --output PATH --upstream-commit HASH " +
|
||||||
|
"--target-framework TFM --generated-root PATH ASSEMBLY...");
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiCatalog.Write(
|
||||||
|
Path.GetFullPath(args[1]),
|
||||||
|
args[3],
|
||||||
|
args[5],
|
||||||
|
Path.GetFullPath(args[7]),
|
||||||
|
args[8..].Select(Path.GetFullPath).ToArray());
|
||||||
|
|
||||||
|
internal static class ApiCatalog
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
WriteIndented = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static void Write(
|
||||||
|
string output,
|
||||||
|
string upstreamCommit,
|
||||||
|
string targetFramework,
|
||||||
|
string generatedRoot,
|
||||||
|
string[] assemblyPaths)
|
||||||
|
{
|
||||||
|
if (assemblyPaths.Length != 13)
|
||||||
|
throw new InvalidOperationException($"expected 13 assemblies, found {assemblyPaths.Length}");
|
||||||
|
|
||||||
|
var pathsByName = Directory
|
||||||
|
.EnumerateFiles(CommonDirectory(assemblyPaths), "*.dll", SearchOption.AllDirectories)
|
||||||
|
.Concat(assemblyPaths)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.Select(path => (Name: Path.GetFileNameWithoutExtension(path), Path: path))
|
||||||
|
.Where(item => item.Name is not null)
|
||||||
|
.GroupBy(item => item.Name!, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(group => group.Key, group => group.Select(item => item.Path).Order(StringComparer.Ordinal).First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
AssemblyLoadContext.Default.Resolving += (_, name) =>
|
||||||
|
name.Name is not null && pathsByName.TryGetValue(name.Name, out var path)
|
||||||
|
? AssemblyLoadContext.Default.LoadFromAssemblyPath(path)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var catalogAssemblyNames = assemblyPaths
|
||||||
|
.Select(path => AssemblyName.GetAssemblyName(path).Name!)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
var externalTypes = new Dictionary<(string Assembly, string DocId), ExternalTypeBuilder>();
|
||||||
|
var assemblies = assemblyPaths
|
||||||
|
.Order(StringComparer.Ordinal)
|
||||||
|
.Select(path => ReadAssembly(path, catalogAssemblyNames, externalTypes))
|
||||||
|
.OrderBy(assembly => assembly.Identity.Name, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
var generators = ReadGenerators(generatedRoot);
|
||||||
|
var externalTypeCatalog = externalTypes.Values
|
||||||
|
.Select(builder => builder.Build())
|
||||||
|
.OrderBy(type => type.Assembly.Name, StringComparer.Ordinal)
|
||||||
|
.ThenBy(type => type.DocId, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
var catalog = new Catalog(
|
||||||
|
1,
|
||||||
|
new Upstream(
|
||||||
|
"https://github.com/cinderblocks/libremetaverse",
|
||||||
|
upstreamCommit,
|
||||||
|
targetFramework),
|
||||||
|
generators,
|
||||||
|
new Summary(
|
||||||
|
assemblies.Length,
|
||||||
|
generators.Length,
|
||||||
|
assemblies.Sum(assembly => assembly.Counts.CatalogTypes),
|
||||||
|
assemblies.Sum(assembly => assembly.Counts.CatalogMembers),
|
||||||
|
externalTypeCatalog.Length),
|
||||||
|
assemblies,
|
||||||
|
externalTypeCatalog);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
|
||||||
|
File.WriteAllText(output, JsonSerializer.Serialize(catalog, JsonOptions) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AssemblyCatalog ReadAssembly(
|
||||||
|
string path,
|
||||||
|
HashSet<string> catalogAssemblyNames,
|
||||||
|
Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
|
||||||
|
{
|
||||||
|
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
|
||||||
|
var identity = assembly.GetName();
|
||||||
|
var types = assembly.GetExportedTypes()
|
||||||
|
.Where(IsPubliclyVisible)
|
||||||
|
.OrderBy(TypeDocId, StringComparer.Ordinal)
|
||||||
|
.Select(type =>
|
||||||
|
{
|
||||||
|
CollectExternalSurface(type, catalogAssemblyNames, externalTypes);
|
||||||
|
return ReadType(type);
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
var duplicateDocIds = types.Select(type => type.DocId)
|
||||||
|
.Concat(types.SelectMany(type => type.Members.Select(member => member.DocId)))
|
||||||
|
.GroupBy(docId => docId, StringComparer.Ordinal)
|
||||||
|
.Where(group => group.Count() > 1)
|
||||||
|
.Select(group => group.Key)
|
||||||
|
.ToArray();
|
||||||
|
if (duplicateDocIds.Length != 0)
|
||||||
|
throw new InvalidOperationException($"{identity.Name}: duplicate documentation IDs: {string.Join(", ", duplicateDocIds)}");
|
||||||
|
var metadataCounts = ReadMetadataCounts(path);
|
||||||
|
var catalogMembers = types.Sum(type => type.Members.Length);
|
||||||
|
if (types.Length != metadataCounts.Types || catalogMembers != metadataCounts.Members)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{identity.Name}: catalog/metadata mismatch: " +
|
||||||
|
$"types {types.Length}/{metadataCounts.Types}, members {catalogMembers}/{metadataCounts.Members}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AssemblyCatalog(
|
||||||
|
new AssemblyIdentity(
|
||||||
|
identity.Name!,
|
||||||
|
identity.Version?.ToString() ?? "0.0.0.0",
|
||||||
|
string.IsNullOrEmpty(identity.CultureName) ? "neutral" : identity.CultureName,
|
||||||
|
Convert.ToHexString(identity.GetPublicKeyToken() ?? []).ToLowerInvariant(),
|
||||||
|
assembly.ManifestModule.ModuleVersionId.ToString("D"),
|
||||||
|
Path.GetFileName(path),
|
||||||
|
Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant()),
|
||||||
|
new Counts(types.Length, catalogMembers, metadataCounts.Types, metadataCounts.Members),
|
||||||
|
types);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypeCatalog ReadType(Type type)
|
||||||
|
{
|
||||||
|
var nullability = new NullabilityInfoContext();
|
||||||
|
var members = new List<MemberCatalog>();
|
||||||
|
|
||||||
|
foreach (var constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||||
|
members.Add(ReadMethod(type, constructor, "constructor", nullability));
|
||||||
|
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(method => !IsAccessor(method)))
|
||||||
|
members.Add(ReadMethod(type, method, "method", nullability));
|
||||||
|
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(property => IsPublic(property.GetMethod) || IsPublic(property.SetMethod)))
|
||||||
|
members.Add(ReadProperty(type, property, nullability));
|
||||||
|
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(field => !field.IsSpecialName))
|
||||||
|
members.Add(ReadField(type, field, nullability));
|
||||||
|
foreach (var @event in type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(@event => IsPublic(@event.AddMethod) || IsPublic(@event.RemoveMethod) || IsPublic(@event.RaiseMethod)))
|
||||||
|
members.Add(ReadEvent(type, @event, nullability));
|
||||||
|
|
||||||
|
var delegateInvoke = typeof(MulticastDelegate).IsAssignableFrom(type.BaseType)
|
||||||
|
? type.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||||
|
: null;
|
||||||
|
return new TypeCatalog(
|
||||||
|
TypeDocId(type),
|
||||||
|
type.Namespace ?? "",
|
||||||
|
type.Name,
|
||||||
|
DisplayType(type),
|
||||||
|
TypeKind(type),
|
||||||
|
type.IsNested ? "nested_public" : "public",
|
||||||
|
type.IsAbstract,
|
||||||
|
type.IsSealed,
|
||||||
|
type.IsAbstract && type.IsSealed,
|
||||||
|
GenericParameters(type.GetGenericArguments().Where(argument => argument.IsGenericParameter)),
|
||||||
|
type.BaseType is null ? null : DisplayType(type.BaseType),
|
||||||
|
type.GetInterfaces().Select(DisplayType).Order(StringComparer.Ordinal).ToArray(),
|
||||||
|
type.IsEnum ? DisplayType(Enum.GetUnderlyingType(type)) : null,
|
||||||
|
delegateInvoke is null ? null : ReadMethod(type, delegateInvoke, "delegate_signature", nullability),
|
||||||
|
Attributes(type.CustomAttributes),
|
||||||
|
members.OrderBy(member => member.DocId, StringComparer.Ordinal).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MemberCatalog ReadMethod(Type owner, MethodBase method, string kind, NullabilityInfoContext nullability)
|
||||||
|
{
|
||||||
|
var methodInfo = method as MethodInfo;
|
||||||
|
return new MemberCatalog(
|
||||||
|
MemberDocId(owner, method),
|
||||||
|
kind,
|
||||||
|
method.IsConstructor ? owner.Name : method.Name,
|
||||||
|
method.IsStatic,
|
||||||
|
Visibility(method),
|
||||||
|
methodInfo is null ? null : DisplayType(methodInfo.ReturnType),
|
||||||
|
methodInfo is null ? null : Nullability(nullability.Create(methodInfo.ReturnParameter)),
|
||||||
|
methodInfo is null ? [] : Attributes(methodInfo.ReturnParameter.CustomAttributes),
|
||||||
|
[],
|
||||||
|
GenericParameters(method.IsGenericMethodDefinition ? method.GetGenericArguments() : []),
|
||||||
|
method.GetParameters().Select(parameter => ReadParameter(parameter, nullability)).ToArray(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Attributes(method.CustomAttributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MemberCatalog ReadProperty(Type owner, PropertyInfo property, NullabilityInfoContext nullability) => new(
|
||||||
|
MemberDocId(owner, property),
|
||||||
|
property.GetIndexParameters().Length == 0 ? "property" : "indexer",
|
||||||
|
property.Name,
|
||||||
|
(property.GetMethod ?? property.SetMethod)!.IsStatic,
|
||||||
|
MostVisible(property.GetMethod, property.SetMethod),
|
||||||
|
DisplayType(property.PropertyType),
|
||||||
|
Nullability(nullability.Create(property)),
|
||||||
|
[],
|
||||||
|
Accessors(("get", property.GetMethod), ("set", property.SetMethod)),
|
||||||
|
[],
|
||||||
|
property.GetIndexParameters().Select(parameter => ReadParameter(parameter, nullability)).ToArray(),
|
||||||
|
VisibilityOrNull(property.GetMethod),
|
||||||
|
VisibilityOrNull(property.SetMethod),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Attributes(property.CustomAttributes));
|
||||||
|
|
||||||
|
private static MemberCatalog ReadField(Type owner, FieldInfo field, NullabilityInfoContext nullability)
|
||||||
|
{
|
||||||
|
var rawValue = field.IsLiteral ? field.GetRawConstantValue() : null;
|
||||||
|
return new MemberCatalog(
|
||||||
|
MemberDocId(owner, field),
|
||||||
|
owner.IsEnum && field.IsLiteral ? "enum_value" : field.IsLiteral ? "constant" : "field",
|
||||||
|
field.Name,
|
||||||
|
field.IsStatic,
|
||||||
|
"public",
|
||||||
|
DisplayType(field.FieldType),
|
||||||
|
Nullability(nullability.Create(field)),
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
field.IsInitOnly,
|
||||||
|
field.IsLiteral,
|
||||||
|
field.IsLiteral ? Constant(field.FieldType, rawValue) : null,
|
||||||
|
owner.IsEnum && field.IsLiteral ? Marshal.SizeOf(Enum.GetUnderlyingType(owner)) * 8 : null,
|
||||||
|
Attributes(field.CustomAttributes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MemberCatalog ReadEvent(Type owner, EventInfo @event, NullabilityInfoContext nullability) => new(
|
||||||
|
MemberDocId(owner, @event),
|
||||||
|
"event",
|
||||||
|
@event.Name,
|
||||||
|
(@event.AddMethod ?? @event.RemoveMethod ?? @event.RaiseMethod)!.IsStatic,
|
||||||
|
MostVisible(@event.AddMethod, @event.RemoveMethod, @event.RaiseMethod),
|
||||||
|
@event.EventHandlerType is null ? null : DisplayType(@event.EventHandlerType),
|
||||||
|
@event.EventHandlerType is null ? null : Nullability(nullability.Create(@event)),
|
||||||
|
[],
|
||||||
|
Accessors(("add", @event.AddMethod), ("remove", @event.RemoveMethod), ("raise", @event.RaiseMethod)),
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
VisibilityOrNull(@event.AddMethod),
|
||||||
|
VisibilityOrNull(@event.RemoveMethod),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Attributes(@event.CustomAttributes));
|
||||||
|
|
||||||
|
private static ParameterCatalog ReadParameter(ParameterInfo parameter, NullabilityInfoContext nullability) => new(
|
||||||
|
parameter.Name ?? $"arg{parameter.Position}",
|
||||||
|
DisplayType(parameter.ParameterType),
|
||||||
|
parameter.IsOut ? "out" : parameter.IsIn && parameter.ParameterType.IsByRef ? "in" : parameter.ParameterType.IsByRef ? "ref" : "value",
|
||||||
|
parameter.IsOptional,
|
||||||
|
parameter.HasDefaultValue ? Constant(parameter.ParameterType, parameter.DefaultValue) : null,
|
||||||
|
Nullability(nullability.Create(parameter)),
|
||||||
|
Attributes(parameter.CustomAttributes));
|
||||||
|
|
||||||
|
private static AccessorCatalog[] Accessors(params (string Kind, MethodInfo? Method)[] accessors) => accessors
|
||||||
|
.Where(accessor => accessor.Method is not null)
|
||||||
|
.Select(accessor => new AccessorCatalog(
|
||||||
|
accessor.Kind,
|
||||||
|
Visibility(accessor.Method!),
|
||||||
|
Attributes(accessor.Method!.CustomAttributes),
|
||||||
|
Attributes(accessor.Method.ReturnParameter.CustomAttributes)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private static GenericParameterCatalog[] GenericParameters(IEnumerable<Type> arguments) => arguments
|
||||||
|
.Select(argument =>
|
||||||
|
{
|
||||||
|
var flags = argument.GenericParameterAttributes;
|
||||||
|
var constraints = argument.GetGenericParameterConstraints().Select(DisplayType).Order(StringComparer.Ordinal).ToArray();
|
||||||
|
return new GenericParameterCatalog(
|
||||||
|
argument.Name,
|
||||||
|
argument.GenericParameterPosition,
|
||||||
|
flags.HasFlag(GenericParameterAttributes.Covariant) ? "out" : flags.HasFlag(GenericParameterAttributes.Contravariant) ? "in" : "invariant",
|
||||||
|
flags.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint),
|
||||||
|
flags.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint),
|
||||||
|
flags.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint),
|
||||||
|
flags.HasFlag(GenericParameterAttributes.AllowByRefLike),
|
||||||
|
constraints,
|
||||||
|
Attributes(argument.CustomAttributes));
|
||||||
|
})
|
||||||
|
.OrderBy(argument => argument.Position)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private static AttributeCatalog[] Attributes(IEnumerable<CustomAttributeData> attributes) => attributes
|
||||||
|
.Select(attribute => new AttributeCatalog(
|
||||||
|
DisplayType(attribute.AttributeType),
|
||||||
|
attribute.ConstructorArguments.Select(AttributeValue).ToArray(),
|
||||||
|
attribute.NamedArguments.OrderBy(argument => argument.MemberName, StringComparer.Ordinal)
|
||||||
|
.Select(argument => new NamedAttributeValue(argument.MemberName, AttributeValue(argument.TypedValue))).ToArray()))
|
||||||
|
.OrderBy(attribute => attribute.Type, StringComparer.Ordinal)
|
||||||
|
.ThenBy(attribute => JsonSerializer.Serialize(attribute, JsonOptions), StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private static TypedValue AttributeValue(CustomAttributeTypedArgument argument) => new(
|
||||||
|
DisplayType(argument.ArgumentType),
|
||||||
|
argument.Value is IEnumerable<CustomAttributeTypedArgument> values
|
||||||
|
? values.Select(AttributeValue).ToArray()
|
||||||
|
: ConstantText(argument.Value));
|
||||||
|
|
||||||
|
private static ConstantValue Constant(Type declaredType, object? value) => new(
|
||||||
|
DisplayType(declaredType.IsByRef ? declaredType.GetElementType()! : declaredType),
|
||||||
|
ConstantText(value));
|
||||||
|
|
||||||
|
private static object? ConstantText(object? value) => value switch
|
||||||
|
{
|
||||||
|
null or DBNull or Missing => null,
|
||||||
|
Type type => DisplayType(type),
|
||||||
|
char character => character.ToString(),
|
||||||
|
float number => number.ToString("R", CultureInfo.InvariantCulture),
|
||||||
|
double number => number.ToString("R", CultureInfo.InvariantCulture),
|
||||||
|
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
||||||
|
_ => value.ToString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static NullabilityCatalog Nullability(NullabilityInfo info) => new(
|
||||||
|
info.ReadState.ToString().ToLowerInvariant(),
|
||||||
|
info.WriteState.ToString().ToLowerInvariant(),
|
||||||
|
info.ElementType is null ? null : Nullability(info.ElementType),
|
||||||
|
info.GenericTypeArguments.Select(Nullability).ToArray());
|
||||||
|
|
||||||
|
private static GeneratorCatalog[] ReadGenerators(string root)
|
||||||
|
{
|
||||||
|
var expected = new Dictionary<string, string[]>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["AttentionsGenerator"] = ["LindenAttentions.g.cs"],
|
||||||
|
["GenepoolGenerator"] = ["Genepool.g.cs"],
|
||||||
|
["PacketSourceGenerator"] = ["Packets.g.cs"],
|
||||||
|
["SkeletonGenerator"] = ["LindenSkeletonDefault.g.cs"],
|
||||||
|
["TreesGenerator"] = ["GrassDefinitions.g.cs", "TreeDefinitions.g.cs"],
|
||||||
|
["VisualParamGenerator"] = ["VisualParams.g.cs"],
|
||||||
|
};
|
||||||
|
var files = Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories).ToArray();
|
||||||
|
return expected.Select(pair =>
|
||||||
|
{
|
||||||
|
var outputs = pair.Value.Select(name => files.SingleOrDefault(path => Path.GetFileName(path) == name)
|
||||||
|
?? throw new InvalidOperationException($"{pair.Key} did not emit {name}"))
|
||||||
|
.Select(path => new GeneratorOutput(
|
||||||
|
Path.GetRelativePath(root, path).Replace(Path.DirectorySeparatorChar, '/'),
|
||||||
|
Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))).ToLowerInvariant()))
|
||||||
|
.OrderBy(output => output.Path, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
return new GeneratorCatalog(pair.Key, outputs);
|
||||||
|
}).OrderBy(generator => generator.Name, StringComparer.Ordinal).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollectExternalSurface(
|
||||||
|
Type type,
|
||||||
|
HashSet<string> catalogAssemblyNames,
|
||||||
|
Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
|
||||||
|
{
|
||||||
|
var typeDocId = TypeDocId(type);
|
||||||
|
CollectExternal(type.BaseType, typeDocId, "base_type", catalogAssemblyNames, externalTypes);
|
||||||
|
foreach (var @interface in type.GetInterfaces())
|
||||||
|
CollectExternal(@interface, typeDocId, "interface", catalogAssemblyNames, externalTypes);
|
||||||
|
foreach (var argument in type.GetGenericArguments().Where(argument => argument.IsGenericParameter))
|
||||||
|
foreach (var constraint in argument.GetGenericParameterConstraints())
|
||||||
|
CollectExternal(constraint, typeDocId, $"generic_constraint:{argument.Name}", catalogAssemblyNames, externalTypes);
|
||||||
|
foreach (var constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||||
|
CollectExternalMethod(type, constructor, catalogAssemblyNames, externalTypes);
|
||||||
|
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(method => !IsAccessor(method)))
|
||||||
|
CollectExternalMethod(type, method, catalogAssemblyNames, externalTypes);
|
||||||
|
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(property => IsPublic(property.GetMethod) || IsPublic(property.SetMethod)))
|
||||||
|
{
|
||||||
|
var docId = MemberDocId(type, property);
|
||||||
|
CollectExternal(property.PropertyType, docId, "property_type", catalogAssemblyNames, externalTypes);
|
||||||
|
foreach (var parameter in property.GetIndexParameters())
|
||||||
|
CollectExternal(parameter.ParameterType, docId, $"parameter:{parameter.Name}", catalogAssemblyNames, externalTypes);
|
||||||
|
}
|
||||||
|
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(field => !field.IsSpecialName))
|
||||||
|
{
|
||||||
|
var docId = MemberDocId(type, field);
|
||||||
|
CollectExternal(field.FieldType, docId, "field_type", catalogAssemblyNames, externalTypes);
|
||||||
|
}
|
||||||
|
foreach (var @event in type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(@event => IsPublic(@event.AddMethod) || IsPublic(@event.RemoveMethod) || IsPublic(@event.RaiseMethod)))
|
||||||
|
{
|
||||||
|
var docId = MemberDocId(type, @event);
|
||||||
|
CollectExternal(@event.EventHandlerType, docId, "event_type", catalogAssemblyNames, externalTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollectExternalMethod(
|
||||||
|
Type owner,
|
||||||
|
MethodBase method,
|
||||||
|
HashSet<string> catalogAssemblyNames,
|
||||||
|
Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
|
||||||
|
{
|
||||||
|
var docId = MemberDocId(owner, method);
|
||||||
|
if (method is MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
CollectExternal(methodInfo.ReturnType, docId, "return_type", catalogAssemblyNames, externalTypes);
|
||||||
|
}
|
||||||
|
foreach (var parameter in method.GetParameters())
|
||||||
|
{
|
||||||
|
CollectExternal(parameter.ParameterType, docId, $"parameter:{parameter.Name}", catalogAssemblyNames, externalTypes);
|
||||||
|
}
|
||||||
|
if (method is MethodInfo genericMethod)
|
||||||
|
foreach (var argument in genericMethod.GetGenericArguments().Where(argument => argument.IsGenericParameter))
|
||||||
|
foreach (var constraint in argument.GetGenericParameterConstraints())
|
||||||
|
CollectExternal(constraint, docId, $"generic_constraint:{argument.Name}", catalogAssemblyNames, externalTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollectExternal(
|
||||||
|
Type? type,
|
||||||
|
string ownerDocId,
|
||||||
|
string position,
|
||||||
|
HashSet<string> catalogAssemblyNames,
|
||||||
|
Dictionary<(string Assembly, string DocId), ExternalTypeBuilder> externalTypes)
|
||||||
|
{
|
||||||
|
if (type is null || type.IsGenericParameter) return;
|
||||||
|
if (type.HasElementType)
|
||||||
|
{
|
||||||
|
CollectExternal(type.GetElementType(), ownerDocId, position, catalogAssemblyNames, externalTypes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var usageSignature = DisplayType(type);
|
||||||
|
if (type.IsGenericType)
|
||||||
|
foreach (var argument in type.GetGenericArguments())
|
||||||
|
CollectExternal(argument, ownerDocId, position, catalogAssemblyNames, externalTypes);
|
||||||
|
var definition = type.IsGenericType ? type.GetGenericTypeDefinition() : type;
|
||||||
|
var assemblyName = definition.Assembly.GetName();
|
||||||
|
if (assemblyName.Name is null || catalogAssemblyNames.Contains(assemblyName.Name)) return;
|
||||||
|
var key = (assemblyName.Name, TypeDocId(definition));
|
||||||
|
if (!externalTypes.TryGetValue(key, out var builder))
|
||||||
|
{
|
||||||
|
builder = new ExternalTypeBuilder(definition);
|
||||||
|
externalTypes.Add(key, builder);
|
||||||
|
}
|
||||||
|
builder.Usages.Add(new ExternalTypeUsage(ownerDocId, position, usageSignature));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MetadataCounts ReadMetadataCounts(string path)
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(path);
|
||||||
|
using var peReader = new PEReader(stream);
|
||||||
|
var metadata = peReader.GetMetadataReader();
|
||||||
|
var visibleTypes = metadata.TypeDefinitions.Where(handle => IsPubliclyVisible(metadata, handle)).ToHashSet();
|
||||||
|
var members = 0;
|
||||||
|
foreach (var handle in visibleTypes)
|
||||||
|
{
|
||||||
|
var definition = metadata.GetTypeDefinition(handle);
|
||||||
|
var accessors = definition.GetProperties()
|
||||||
|
.SelectMany(property => PropertyAccessors(metadata.GetPropertyDefinition(property)))
|
||||||
|
.Concat(definition.GetEvents().SelectMany(@event => EventAccessors(metadata.GetEventDefinition(@event))))
|
||||||
|
.ToHashSet();
|
||||||
|
members += definition.GetMethods().Count(method =>
|
||||||
|
IsPublic(metadata.GetMethodDefinition(method).Attributes) && !accessors.Contains(method));
|
||||||
|
members += definition.GetFields().Count(field =>
|
||||||
|
{
|
||||||
|
var attributes = metadata.GetFieldDefinition(field).Attributes;
|
||||||
|
return (attributes & FieldAttributes.FieldAccessMask) == FieldAttributes.Public &&
|
||||||
|
!attributes.HasFlag(FieldAttributes.SpecialName);
|
||||||
|
});
|
||||||
|
members += definition.GetProperties().Count(property =>
|
||||||
|
PropertyAccessors(metadata.GetPropertyDefinition(property)).Any(accessor => IsPublic(metadata.GetMethodDefinition(accessor).Attributes)));
|
||||||
|
members += definition.GetEvents().Count(@event =>
|
||||||
|
EventAccessors(metadata.GetEventDefinition(@event)).Any(accessor => IsPublic(metadata.GetMethodDefinition(accessor).Attributes)));
|
||||||
|
}
|
||||||
|
return new MetadataCounts(visibleTypes.Count, members);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<MethodDefinitionHandle> PropertyAccessors(PropertyDefinition property)
|
||||||
|
{
|
||||||
|
var accessors = property.GetAccessors();
|
||||||
|
if (!accessors.Getter.IsNil) yield return accessors.Getter;
|
||||||
|
if (!accessors.Setter.IsNil) yield return accessors.Setter;
|
||||||
|
foreach (var other in accessors.Others) yield return other;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<MethodDefinitionHandle> EventAccessors(EventDefinition @event)
|
||||||
|
{
|
||||||
|
var accessors = @event.GetAccessors();
|
||||||
|
if (!accessors.Adder.IsNil) yield return accessors.Adder;
|
||||||
|
if (!accessors.Remover.IsNil) yield return accessors.Remover;
|
||||||
|
if (!accessors.Raiser.IsNil) yield return accessors.Raiser;
|
||||||
|
foreach (var other in accessors.Others) yield return other;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPubliclyVisible(MetadataReader metadata, TypeDefinitionHandle handle)
|
||||||
|
{
|
||||||
|
var definition = metadata.GetTypeDefinition(handle);
|
||||||
|
var visibility = definition.Attributes & TypeAttributes.VisibilityMask;
|
||||||
|
if (visibility == TypeAttributes.Public) return true;
|
||||||
|
if (visibility != TypeAttributes.NestedPublic) return false;
|
||||||
|
var parent = definition.GetDeclaringType();
|
||||||
|
return !parent.IsNil && IsPubliclyVisible(metadata, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPubliclyVisible(Type type) => type.IsPublic || type.IsNestedPublic && type.DeclaringType is not null && IsPubliclyVisible(type.DeclaringType);
|
||||||
|
private static bool IsPublic(MethodBase? method) => method?.IsPublic == true;
|
||||||
|
private static bool IsPublic(MethodAttributes attributes) => (attributes & MethodAttributes.MemberAccessMask) == MethodAttributes.Public;
|
||||||
|
private static bool IsAccessor(MethodInfo method) => method.IsSpecialName &&
|
||||||
|
(method.Name.StartsWith("get_", StringComparison.Ordinal) || method.Name.StartsWith("set_", StringComparison.Ordinal) ||
|
||||||
|
method.Name.StartsWith("add_", StringComparison.Ordinal) || method.Name.StartsWith("remove_", StringComparison.Ordinal) ||
|
||||||
|
method.Name.StartsWith("raise_", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
private static string TypeKind(Type type) => type.IsEnum ? "enum" :
|
||||||
|
typeof(MulticastDelegate).IsAssignableFrom(type.BaseType) ? "delegate" :
|
||||||
|
type.IsInterface ? "interface" : type.IsValueType ? "struct" : "class";
|
||||||
|
|
||||||
|
private static string CommonDirectory(string[] paths)
|
||||||
|
{
|
||||||
|
var common = Path.GetDirectoryName(paths[0])!;
|
||||||
|
while (paths.Any(path => !Path.GetFullPath(path).StartsWith(common + Path.DirectorySeparatorChar, StringComparison.Ordinal)))
|
||||||
|
common = Path.GetDirectoryName(common) ?? throw new InvalidOperationException("assemblies have no common directory");
|
||||||
|
return common;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Visibility(MethodBase method) => method.IsPublic ? "public" : method.IsFamily ? "protected" :
|
||||||
|
method.IsFamilyOrAssembly ? "protected_internal" : method.IsAssembly ? "internal" : "private";
|
||||||
|
private static string? VisibilityOrNull(MethodBase? method) => method is null ? null : Visibility(method);
|
||||||
|
private static string MostVisible(params MethodBase?[] methods) => methods.Any(IsPublic) ? "public" :
|
||||||
|
methods.Any(method => method?.IsFamilyOrAssembly == true) ? "protected_internal" :
|
||||||
|
methods.Any(method => method?.IsFamily == true) ? "protected" : "private";
|
||||||
|
|
||||||
|
private static string DisplayType(Type type)
|
||||||
|
{
|
||||||
|
if (type.IsByRef) return $"{DisplayType(type.GetElementType()!)}&";
|
||||||
|
if (type.IsPointer) return $"{DisplayType(type.GetElementType()!)}*";
|
||||||
|
if (type.IsArray) return $"{DisplayType(type.GetElementType()!)}[{new string(',', type.GetArrayRank() - 1)}]";
|
||||||
|
if (type.IsGenericParameter) return type.Name;
|
||||||
|
var name = (type.IsNested ? $"{DisplayType(type.DeclaringType!)}.{type.Name}" : $"{type.Namespace}.{type.Name}").TrimStart('.');
|
||||||
|
var tick = name.IndexOf('`');
|
||||||
|
if (tick >= 0) name = name[..tick];
|
||||||
|
if (!type.IsGenericType) return name;
|
||||||
|
var ownArguments = type.GetGenericArguments();
|
||||||
|
if (type.IsNested && type.DeclaringType is { IsGenericType: true } declaring)
|
||||||
|
ownArguments = ownArguments.Skip(declaring.GetGenericArguments().Length).ToArray();
|
||||||
|
return ownArguments.Length == 0 ? name : $"{name}<{string.Join(", ", ownArguments.Select(DisplayType))}>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TypeDocId(Type type) => $"T:{DocTypeDefinitionName(type)}";
|
||||||
|
private static string MemberDocId(Type owner, MemberInfo member)
|
||||||
|
{
|
||||||
|
var prefix = member switch
|
||||||
|
{
|
||||||
|
PropertyInfo => "P",
|
||||||
|
FieldInfo => "F",
|
||||||
|
EventInfo => "E",
|
||||||
|
_ => "M",
|
||||||
|
};
|
||||||
|
var name = member is ConstructorInfo ? "#ctor" : member.Name.Replace('.', '#');
|
||||||
|
if (member is MethodInfo { IsGenericMethodDefinition: true } method)
|
||||||
|
name += $"``{method.GetGenericArguments().Length}";
|
||||||
|
var parameters = member switch
|
||||||
|
{
|
||||||
|
MethodBase methodBase => methodBase.GetParameters(),
|
||||||
|
PropertyInfo property => property.GetIndexParameters(),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
var suffix = parameters.Length == 0 ? "" : $"({string.Join(",", parameters.Select(parameter => DocParameterType(parameter.ParameterType)))})";
|
||||||
|
if (member is MethodInfo { Name: "op_Implicit" or "op_Explicit" } conversion)
|
||||||
|
suffix += $"~{DocParameterType(conversion.ReturnType)}";
|
||||||
|
return $"{prefix}:{DocTypeDefinitionName(owner)}.{name}{suffix}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DocTypeDefinitionName(Type type)
|
||||||
|
{
|
||||||
|
var ownName = type.Name;
|
||||||
|
return type.IsNested
|
||||||
|
? $"{DocTypeDefinitionName(type.DeclaringType!)}.{ownName}"
|
||||||
|
: string.IsNullOrEmpty(type.Namespace) ? ownName : $"{type.Namespace}.{ownName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DocParameterType(Type type)
|
||||||
|
{
|
||||||
|
if (type.IsByRef) return $"{DocParameterType(type.GetElementType()!)}@";
|
||||||
|
if (type.IsPointer) return $"{DocParameterType(type.GetElementType()!)}*";
|
||||||
|
if (type.IsArray)
|
||||||
|
return type.GetArrayRank() == 1
|
||||||
|
? $"{DocParameterType(type.GetElementType()!)}[]"
|
||||||
|
: $"{DocParameterType(type.GetElementType()!)}[{string.Join(",", Enumerable.Repeat("0:", type.GetArrayRank()))}]";
|
||||||
|
if (type.IsGenericParameter)
|
||||||
|
return type.DeclaringMethod is null ? $"`{type.GenericParameterPosition}" : $"``{type.GenericParameterPosition}";
|
||||||
|
var name = type.IsNested
|
||||||
|
? $"{DocParameterType(type.DeclaringType!)}.{type.Name}"
|
||||||
|
: string.IsNullOrEmpty(type.Namespace) ? type.Name : $"{type.Namespace}.{type.Name}";
|
||||||
|
var tick = name.IndexOf('`');
|
||||||
|
if (tick >= 0) name = name[..tick];
|
||||||
|
if (!type.IsGenericType) return name;
|
||||||
|
var arguments = type.GetGenericArguments();
|
||||||
|
return $"{name}{{{string.Join(",", arguments.Select(DocParameterType))}}}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void SelfTest()
|
||||||
|
{
|
||||||
|
var method = typeof(SelfTestFixture<>).GetMethod(nameof(SelfTestFixture<int>.Convert))!;
|
||||||
|
var expected = "M:ApiCatalog.SelfTestFixture`1.Convert``1(System.Collections.Generic.Dictionary{`0,``0[]},System.Int32@)";
|
||||||
|
if (MemberDocId(typeof(SelfTestFixture<>), method) != expected)
|
||||||
|
throw new InvalidOperationException($"documentation ID mismatch: {MemberDocId(typeof(SelfTestFixture<>), method)}");
|
||||||
|
var nullability = new NullabilityInfoContext().Create(method.GetParameters()[0]);
|
||||||
|
if (nullability.GenericTypeArguments.Length != 2 || nullability.GenericTypeArguments[1].ElementType?.ReadState != NullabilityState.Nullable)
|
||||||
|
throw new InvalidOperationException("nested nullability metadata was not preserved");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SelfTestFixture<T> where T : notnull
|
||||||
|
{
|
||||||
|
public static explicit operator string(SelfTestFixture<T> _) => "";
|
||||||
|
public string Convert<TMethod>(Dictionary<T, TMethod?[]> value, ref int count) where TMethod : class => "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record Catalog(
|
||||||
|
int SchemaVersion, Upstream Upstream, GeneratorCatalog[] SourceGenerators, Summary Summary,
|
||||||
|
AssemblyCatalog[] Assemblies, ExternalTypeCatalog[] ExternalTypes);
|
||||||
|
private sealed record Upstream(string Repository, string Commit, string TargetFramework);
|
||||||
|
private sealed record Summary(int AssemblyCount, int SourceGeneratorCount, int PublicTypeCount, int PublicMemberCount, int ExternalTypeCount);
|
||||||
|
private sealed record GeneratorCatalog(string Name, GeneratorOutput[] Outputs);
|
||||||
|
private sealed record GeneratorOutput(string Path, string Sha256);
|
||||||
|
private sealed record AssemblyCatalog(AssemblyIdentity Identity, Counts Counts, TypeCatalog[] Types);
|
||||||
|
private sealed record AssemblyIdentity(string Name, string Version, string Culture, string PublicKeyToken, string Mvid, string File, string Sha256);
|
||||||
|
private sealed record Counts(int CatalogTypes, int CatalogMembers, int MetadataTypes, int MetadataMembers);
|
||||||
|
private sealed record TypeCatalog(
|
||||||
|
string DocId, string Namespace, string Name, string Signature, string Kind, string Visibility,
|
||||||
|
bool Abstract, bool Sealed, bool Static, GenericParameterCatalog[] GenericParameters,
|
||||||
|
string? BaseType, string[] Interfaces, string? EnumUnderlyingType, MemberCatalog? DelegateSignature,
|
||||||
|
AttributeCatalog[] Attributes, MemberCatalog[] Members);
|
||||||
|
private sealed record MemberCatalog(
|
||||||
|
string DocId, string Kind, string Name, bool Static, string Visibility, string? Type,
|
||||||
|
NullabilityCatalog? Nullability, AttributeCatalog[] ReturnAttributes, AccessorCatalog[] Accessors,
|
||||||
|
GenericParameterCatalog[] GenericParameters, ParameterCatalog[] Parameters,
|
||||||
|
string? GetterVisibility, string? SetterVisibility, bool? ReadOnly, bool? Constant,
|
||||||
|
ConstantValue? Value, int? NumericWidth, AttributeCatalog[] Attributes);
|
||||||
|
private sealed record AccessorCatalog(string Kind, string Visibility, AttributeCatalog[] Attributes, AttributeCatalog[] ReturnAttributes);
|
||||||
|
private sealed record ParameterCatalog(
|
||||||
|
string Name, string Type, string Passing, bool Optional, ConstantValue? Default,
|
||||||
|
NullabilityCatalog Nullability, AttributeCatalog[] Attributes);
|
||||||
|
private sealed record GenericParameterCatalog(
|
||||||
|
string Name, int Position, string Variance, bool ReferenceType, bool ValueType,
|
||||||
|
bool DefaultConstructor, bool AllowByRefLike, string[] Constraints, AttributeCatalog[] Attributes);
|
||||||
|
private sealed record NullabilityCatalog(string Read, string Write, NullabilityCatalog? Element, NullabilityCatalog[] GenericArguments);
|
||||||
|
private sealed record AttributeCatalog(string Type, TypedValue[] ConstructorArguments, NamedAttributeValue[] NamedArguments);
|
||||||
|
private sealed record NamedAttributeValue(string Name, TypedValue Value);
|
||||||
|
private sealed record TypedValue(string Type, object? Value);
|
||||||
|
private sealed record ConstantValue(string Type, object? Value);
|
||||||
|
private sealed record ExternalTypeCatalog(
|
||||||
|
string DocId, string Signature, string Kind, ExternalAssemblyIdentity Assembly,
|
||||||
|
string MappingStatus, ExternalTypeUsage[] Usages);
|
||||||
|
private sealed record ExternalAssemblyIdentity(string Name, string Version, string Culture, string PublicKeyToken);
|
||||||
|
private sealed record ExternalTypeUsage(string OwnerDocId, string Position, string Signature);
|
||||||
|
private sealed class ExternalTypeBuilder(Type type)
|
||||||
|
{
|
||||||
|
internal HashSet<ExternalTypeUsage> Usages { get; } = [];
|
||||||
|
|
||||||
|
internal ExternalTypeCatalog Build()
|
||||||
|
{
|
||||||
|
var identity = type.Assembly.GetName();
|
||||||
|
return new ExternalTypeCatalog(
|
||||||
|
TypeDocId(type),
|
||||||
|
DisplayType(type),
|
||||||
|
TypeKind(type),
|
||||||
|
new ExternalAssemblyIdentity(
|
||||||
|
identity.Name!,
|
||||||
|
identity.Version?.ToString() ?? "0.0.0.0",
|
||||||
|
string.IsNullOrEmpty(identity.CultureName) ? "neutral" : identity.CultureName,
|
||||||
|
Convert.ToHexString(identity.GetPublicKeyToken() ?? []).ToLowerInvariant()),
|
||||||
|
"pending_rust_mapping",
|
||||||
|
Usages.OrderBy(usage => usage.OwnerDocId, StringComparer.Ordinal)
|
||||||
|
.ThenBy(usage => usage.Position, StringComparer.Ordinal)
|
||||||
|
.ThenBy(usage => usage.Signature, StringComparer.Ordinal)
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private readonly record struct MetadataCounts(int Types, int Members);
|
||||||
|
}
|
||||||
127
tools/extract_public_api.py
Normal file
127
tools/extract_public_api.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build pinned LibreMetaverse metadata twice and emit a deterministic API catalog."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import filecmp
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
UPSTREAM_COMMIT = "2aa70bb68513b39795da5d13c88f31b86e85a3ba"
|
||||||
|
TARGET_FRAMEWORK = "net10.0"
|
||||||
|
PROJECTS = (
|
||||||
|
"LibreMetaverse.Types/LibreMetaverse.Types.csproj",
|
||||||
|
"LibreMetaverse.StructuredData/LibreMetaverse.StructuredData.csproj",
|
||||||
|
"LibreMetaverse.Imaging.Abstractions/LibreMetaverse.Imaging.Abstractions.csproj",
|
||||||
|
"LibreMetaverse.Imaging.Skia/LibreMetaverse.Imaging.Skia.csproj",
|
||||||
|
"PrimMesher/LibreMetaverse.PrimMesher.csproj",
|
||||||
|
"LibreMetaverse/LibreMetaverse.csproj",
|
||||||
|
"LibreMetaverse.Rendering.Simple/LibreMetaverse.Rendering.Simple.csproj",
|
||||||
|
"LibreMetaverse.Rendering.MeshFoundry/LibreMetaverse.Rendering.MeshFoundry.csproj",
|
||||||
|
"LibreMetaverse.LslTools/LibreMetaverse.LslTools.csproj",
|
||||||
|
"LibreMetaverse.RLV/LibreMetaverse.RLV.csproj",
|
||||||
|
"LibreMetaverse.Utilities/LibreMetaverse.Utilities.csproj",
|
||||||
|
"LibreMetaverse.Voice.Vivox/LibreMetaverse.Voice.Vivox.csproj",
|
||||||
|
"LibreMetaverse.Voice.WebRTC/LibreMetaverse.Voice.WebRTC.csproj",
|
||||||
|
)
|
||||||
|
ASSEMBLIES = tuple(Path(project).stem for project in PROJECTS)
|
||||||
|
|
||||||
|
|
||||||
|
def run(*command: str, cwd: Path | None = None) -> None:
|
||||||
|
subprocess.run(command, cwd=cwd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def output(command: list[str], cwd: Path) -> str:
|
||||||
|
return subprocess.run(command, cwd=cwd, check=True, text=True, stdout=subprocess.PIPE).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_upstream(upstream: Path) -> None:
|
||||||
|
actual = output(["git", "rev-parse", "HEAD"], upstream)
|
||||||
|
if actual != UPSTREAM_COMMIT:
|
||||||
|
raise RuntimeError(f"expected upstream {UPSTREAM_COMMIT}, found {actual}")
|
||||||
|
status = output(["git", "status", "--porcelain"], upstream)
|
||||||
|
if status:
|
||||||
|
raise RuntimeError("the pinned upstream checkout must be clean")
|
||||||
|
|
||||||
|
|
||||||
|
def build_tool(root: Path, temporary: Path) -> Path:
|
||||||
|
artifacts = temporary / "tool-artifacts"
|
||||||
|
run(
|
||||||
|
"dotnet", "build", str(root / "tools/api-catalog/ApiCatalog.csproj"),
|
||||||
|
"-c", "Release", "--artifacts-path", str(artifacts),
|
||||||
|
)
|
||||||
|
candidates = sorted((artifacts / "bin").rglob("ApiCatalog.dll"))
|
||||||
|
if len(candidates) != 1:
|
||||||
|
raise RuntimeError(f"expected one extractor assembly, found {candidates}")
|
||||||
|
run("dotnet", str(candidates[0]), "--self-test")
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def build_upstream(upstream: Path, temporary: Path) -> tuple[list[Path], Path]:
|
||||||
|
artifacts = temporary / "upstream-artifacts"
|
||||||
|
binaries = temporary / "bin"
|
||||||
|
generated = temporary / "generated"
|
||||||
|
for project in PROJECTS:
|
||||||
|
command = [
|
||||||
|
"dotnet", "build", str(upstream / project), "-c", "Release", "-f", TARGET_FRAMEWORK,
|
||||||
|
"--artifacts-path", str(artifacts), "-p:ContinuousIntegrationBuild=true",
|
||||||
|
f"-p:OutputPath={binaries}/", "-p:CopyLocalLockFileAssemblies=true",
|
||||||
|
]
|
||||||
|
if project == "LibreMetaverse/LibreMetaverse.csproj":
|
||||||
|
command.extend([
|
||||||
|
"--no-incremental", "-p:EmitCompilerGeneratedFiles=true",
|
||||||
|
f"-p:CompilerGeneratedFilesOutputPath={generated}",
|
||||||
|
])
|
||||||
|
run(*command)
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
for name in ASSEMBLIES:
|
||||||
|
path = binaries / f"{name}.dll"
|
||||||
|
if not path.is_file():
|
||||||
|
raise RuntimeError(f"missing built assembly {path}")
|
||||||
|
paths.append(path)
|
||||||
|
return paths, generated
|
||||||
|
|
||||||
|
|
||||||
|
def extract(tool: Path, assemblies: list[Path], generated: Path, destination: Path) -> None:
|
||||||
|
run(
|
||||||
|
"dotnet", str(tool), "--output", str(destination),
|
||||||
|
"--upstream-commit", UPSTREAM_COMMIT,
|
||||||
|
"--target-framework", TARGET_FRAMEWORK,
|
||||||
|
"--generated-root", str(generated),
|
||||||
|
*(str(path) for path in assemblies),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--upstream", type=Path, default=Path("../libremetaverse"))
|
||||||
|
parser.add_argument("--output", type=Path, default=Path("api/public-api.json"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
root = Path(__file__).resolve().parent.parent
|
||||||
|
upstream = args.upstream.resolve()
|
||||||
|
destination = args.output.resolve()
|
||||||
|
validate_upstream(upstream)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="metacrate-api-catalog-") as directory:
|
||||||
|
temporary = Path(directory)
|
||||||
|
tool = build_tool(root, temporary)
|
||||||
|
assemblies, generated = build_upstream(upstream, temporary / "upstream")
|
||||||
|
outputs = []
|
||||||
|
for pass_number in (1, 2):
|
||||||
|
catalog = temporary / f"public-api-{pass_number}.json"
|
||||||
|
extract(tool, assemblies, generated, catalog)
|
||||||
|
outputs.append(catalog)
|
||||||
|
if not filecmp.cmp(outputs[0], outputs[1], shallow=False):
|
||||||
|
raise RuntimeError("two clean extraction passes produced different bytes")
|
||||||
|
validate_upstream(upstream)
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(outputs[0], destination)
|
||||||
|
print(f"wrote deterministic API catalog to {destination}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user