Add authoritative public API catalog
This commit is contained in:
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);
|
||||
}
|
||||
Reference in New Issue
Block a user