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 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(); 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 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 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 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(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 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 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 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 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 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.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 where T : notnull { public static explicit operator string(SelfTestFixture _) => ""; public string Convert(Dictionary 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 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); }