Files
MetaCrate/tools/api-catalog/Program.cs

696 lines
37 KiB
C#

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);
}