@@ -10,10 +10,6 @@ | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="../../tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.Extensions.Hosting" /> | <PackageReference Include="Microsoft.Extensions.Hosting" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
@@ -0,0 +1,241 @@ | |||||
using System; | |||||
using Discord.Net.Serialization; | |||||
namespace Discord.Net.Models | |||||
{ | |||||
/// <summary> | |||||
/// Represents a guild or DM channel within Discord. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// <see href="https://discord.com/developers/docs/resources/channel#channel-object-channel-structure"/> | |||||
/// </remarks> | |||||
/// <param name="Id"> | |||||
/// The id of this channel. | |||||
/// </param> | |||||
/// <param name="Type"> | |||||
/// The type of channel. | |||||
/// </param> | |||||
[DiscriminatedUnion(nameof(Channel.Type))] | |||||
[GenerateSerializer] | |||||
public record Channel( | |||||
ChannelType Type, | |||||
Snowflake Id); | |||||
/// <summary> | |||||
/// Represents a text channel within a server. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildText)] | |||||
[GenerateSerializer] | |||||
public record GuildTextChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
/*Overwrite[] PermissionOverwrites,*/ | |||||
string Name, | |||||
string? Topic, | |||||
bool Nsfw, | |||||
Snowflake LastMessageId, | |||||
int RateLimitPerUser, | |||||
Snowflake? ParentId, | |||||
DateTimeOffset? LastPinTimestamp) | |||||
: Channel( | |||||
ChannelType.GuildText, | |||||
Id); | |||||
/* | |||||
/// <summary> | |||||
/// Represents a direct message between users. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.DM)] | |||||
[GenerateSerializer] | |||||
public record DMChannel( | |||||
Snowflake Id, | |||||
User[] Recipients) | |||||
: Channel( | |||||
ChannelType.DM, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a voice channel within a server. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildVoice)] | |||||
[GenerateSerializer] | |||||
public record GuildVoiceChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, | |||||
string Name, | |||||
bool Nsfw, | |||||
int Bitrate, | |||||
int UserLimit, | |||||
Snowflake? ParentId, | |||||
string? RtcRegion, | |||||
int VideoQualityMode) | |||||
: Channel( | |||||
ChannelType.GuildVoice, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a direct message between multiple users. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GroupDM)] | |||||
[GenerateSerializer] | |||||
public record GroupDMChannel( | |||||
Snowflake Id, | |||||
string Name, | |||||
Snowflake LastMessageId, | |||||
User[] Recipients, | |||||
string? Icon, | |||||
Snowflake? OwnerId, | |||||
Snowflake? ApplicationId, | |||||
DateTimeOffset? LastPinTimestamp) | |||||
: Channel( | |||||
ChannelType.GroupDM, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents an organizational category that contains up to 50 channels. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildCategory)] | |||||
[GenerateSerializer] | |||||
public record GuildCategoryChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, | |||||
string Name) | |||||
: Channel( | |||||
ChannelType.GuildCategory, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a channel that users can follow and crosspost into their own | |||||
/// server. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildNews)] | |||||
[GenerateSerializer] | |||||
public record GuildNewsChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, | |||||
string Name, | |||||
string? Topic, | |||||
bool Nsfw, | |||||
Snowflake? LastMessageId, | |||||
int RateLimitPerUser, | |||||
Snowflake? ParentId, | |||||
Snowflake? LastPinTimestamp) | |||||
: Channel( | |||||
ChannelType.GuildNews, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a channel in which game developers can sell their game on | |||||
/// Discord. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildStore)] | |||||
[GenerateSerializer] | |||||
public record GuildStoreChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, // I guess??? | |||||
string? Name, | |||||
Snowflake? ParentId) | |||||
: Channel( | |||||
ChannelType.GuildStore, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a temporary sub-channel within a | |||||
/// <see cref="GuildNewsChannel"/>. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildNewsThread)] | |||||
[GenerateSerializer] | |||||
public record GuildNewsThreadChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, // I guess?? | |||||
string Name, | |||||
Snowflake? LastMessageId, | |||||
Snowflake? ParentId, | |||||
Snowflake? LastPinTimestamp, | |||||
int MessageCount, | |||||
int MemberCount, | |||||
ThreadMetadata ThreadMetadata, | |||||
ThreadMember Member) | |||||
: Channel( | |||||
ChannelType.GuildNewsThread, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a temporary sub-channel within a | |||||
/// <see cref="GuildTextChannel"/>. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildPublicThread)] | |||||
[GenerateSerializer] | |||||
public record GuildPublicThreadChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, // I guess?? | |||||
string Name, | |||||
Snowflake? LastMessageId, | |||||
Snowflake? ParentId, | |||||
Snowflake? LastPinTimestamp, | |||||
int MessageCount, | |||||
int MemberCount, | |||||
ThreadMetadata ThreadMetadata, | |||||
ThreadMember Member) | |||||
: Channel( | |||||
ChannelType.GuildPublicThread, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a temporary sub-channel within a | |||||
/// <see cref="GuildTextChannel"/> that is only viewable by those invited | |||||
/// and those with the MANAGE_THREADS permission. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildPrivateThread)] | |||||
[GenerateSerializer] | |||||
public record GuildPrivateThreadChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, // I guess??? | |||||
string Name, | |||||
Snowflake? LastMessageId, | |||||
Snowflake? ParentId, | |||||
Snowflake? LastPinTimestamp, | |||||
int MessageCount, | |||||
int MemberCount, | |||||
ThreadMetadata ThreadMetadata, | |||||
ThreadMember Member) | |||||
: Channel( | |||||
ChannelType.GuildPrivateThread, | |||||
Id); | |||||
/// <summary> | |||||
/// Represents a voice channel for hosting events with an audience. | |||||
/// </summary> | |||||
[DiscriminatedUnionMember(ChannelType.GuildStageVoice)] | |||||
[GenerateSerializer] | |||||
public record GuildStageVoiceChannel( | |||||
Snowflake Id, | |||||
Snowflake GuildId, | |||||
int Position, | |||||
Overwrite[] PermissionOverwrites, | |||||
string Name, | |||||
int Bitrate, | |||||
int UserLimit, | |||||
string? RtcRegion) | |||||
: Channel( | |||||
ChannelType.GuildStageVoice, | |||||
Id); | |||||
*/ | |||||
} |
@@ -0,0 +1,68 @@ | |||||
namespace Discord.Net.Models | |||||
{ | |||||
/// <summary> | |||||
/// Declares an enum which represents the type of a <see cref="Channel"/>. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// <see href="https://discord.com/developers/docs/resources/channel#channel-object-channel-types"/> | |||||
/// </remarks> | |||||
public enum ChannelType | |||||
{ | |||||
/// <summary> | |||||
/// A text channel within a server. | |||||
/// </summary> | |||||
GuildText = 0, | |||||
/// <summary> | |||||
/// A direct message between users. | |||||
/// </summary> | |||||
DM = 1, | |||||
/// <summary> | |||||
/// A voice channel within a server. | |||||
/// </summary> | |||||
GuildVoice = 2, | |||||
/// <summary> | |||||
/// A direct message between multiple users. | |||||
/// </summary> | |||||
GroupDM = 3, | |||||
/// <summary> | |||||
/// An organizational category that contains up to 50 channels. | |||||
/// </summary> | |||||
GuildCategory = 4, | |||||
/// <summary> | |||||
/// A channel that users can follow and crosspost into their own server. | |||||
/// </summary> | |||||
GuildNews = 5, | |||||
/// <summary> | |||||
/// A channel in which game developers can sell their game on Discord. | |||||
/// </summary> | |||||
GuildStore = 6, | |||||
/// <summary> | |||||
/// A temporary sub-channel within a <see cref="GuildNews"/> channel. | |||||
/// </summary> | |||||
GuildNewsThread = 10, | |||||
/// <summary> | |||||
/// A temporary sub-channel within a <see cref="GuildText"/> channel. | |||||
/// </summary> | |||||
GuildPublicThread = 11, | |||||
/// <summary> | |||||
/// A temporary sub-channel within a <see cref="GuildText"/> channel | |||||
/// that is only viewable by those invited and those with the | |||||
/// MANAGE_THREADS permission. | |||||
/// </summary> | |||||
GuildPrivateThread = 12, | |||||
/// <summary> | |||||
/// A voice channel for hosting events with an audience. | |||||
/// </summary> | |||||
GuildStageVoice = 13 | |||||
} | |||||
} |
@@ -7,6 +7,8 @@ | |||||
$(Description) | $(Description) | ||||
Shared models between the Discord REST API and Gateway. | Shared models between the Discord REST API and Gateway. | ||||
</Description> | </Description> | ||||
<DiscordNet_SerializationGenerator_OptionsTypeNamespace>Discord.Net.Serialization</DiscordNet_SerializationGenerator_OptionsTypeNamespace> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -14,6 +16,11 @@ | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<CompilerVisibleProperty Include="DiscordNet_SerializationGenerator_OptionsTypeNamespace" /> | |||||
<ProjectReference Include="../../tools/SourceGenerators/Serialization/Discord.Net.SourceGenerators.Serialization.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="../Serialization/Discord.Net.Serialization.csproj" /> | <ProjectReference Include="../Serialization/Discord.Net.Serialization.csproj" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
@@ -5,6 +5,9 @@ namespace Discord.Net.Serialization | |||||
/// <summary> | /// <summary> | ||||
/// Defines an attribute used to mark discriminated unions. | /// Defines an attribute used to mark discriminated unions. | ||||
/// </summary> | /// </summary> | ||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, | |||||
AllowMultiple = false, | |||||
Inherited = false)] | |||||
public class DiscriminatedUnionAttribute : Attribute | public class DiscriminatedUnionAttribute : Attribute | ||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
@@ -5,12 +5,15 @@ namespace Discord.Net.Serialization | |||||
/// <summary> | /// <summary> | ||||
/// Defines an attribute used to mark members of discriminated unions. | /// Defines an attribute used to mark members of discriminated unions. | ||||
/// </summary> | /// </summary> | ||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, | |||||
AllowMultiple = false, | |||||
Inherited = false)] | |||||
public class DiscriminatedUnionMemberAttribute : Attribute | public class DiscriminatedUnionMemberAttribute : Attribute | ||||
{ | { | ||||
/// <summary> | /// <summary> | ||||
/// Gets the discriminator value used to identify this member type. | /// Gets the discriminator value used to identify this member type. | ||||
/// </summary> | /// </summary> | ||||
public string Discriminator { get; } | |||||
public object Discriminator { get; } | |||||
/// <summary> | /// <summary> | ||||
/// Creates a new <see cref="DiscriminatedUnionMemberAttribute"/> | /// Creates a new <see cref="DiscriminatedUnionMemberAttribute"/> | ||||
@@ -19,7 +22,7 @@ namespace Discord.Net.Serialization | |||||
/// <param name="discriminator"> | /// <param name="discriminator"> | ||||
/// The discriminator value used to identify this member type. | /// The discriminator value used to identify this member type. | ||||
/// </param> | /// </param> | ||||
public DiscriminatedUnionMemberAttribute(string discriminator) | |||||
public DiscriminatedUnionMemberAttribute(object discriminator) | |||||
{ | { | ||||
Discriminator = discriminator; | Discriminator = discriminator; | ||||
} | } | ||||
@@ -0,0 +1,16 @@ | |||||
using System; | |||||
namespace Discord.Net.Serialization | |||||
{ | |||||
/// <summary> | |||||
/// Defines an attribute which informs the serializer generator to generate | |||||
/// a serializer for this type. | |||||
/// </summary> | |||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, | |||||
AllowMultiple = false, | |||||
Inherited = false)] | |||||
public class GenerateSerializerAttribute : Attribute | |||||
{ | |||||
} | |||||
} |
@@ -21,6 +21,7 @@ | |||||
<PropertyGroup> | <PropertyGroup> | ||||
<GenerateDocumentationFile>false</GenerateDocumentationFile> | <GenerateDocumentationFile>false</GenerateDocumentationFile> | ||||
<NoPackageAnalysis>true</NoPackageAnalysis> | <NoPackageAnalysis>true</NoPackageAnalysis> | ||||
<IncludeBuildOutput>false</IncludeBuildOutput> | |||||
<!-- Disable release tracking analyzers due to weird behaviour with OmniSharp --> | <!-- Disable release tracking analyzers due to weird behaviour with OmniSharp --> | ||||
<NoWarn>$(NoWarn);RS2000;RS2001;RS2002;RS2003;RS2004;RS2005;RS2006;RS2007;RS2008</NoWarn> | <NoWarn>$(NoWarn);RS2000;RS2001;RS2002;RS2003;RS2004;RS2005;RS2006;RS2007;RS2008</NoWarn> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
@@ -33,4 +34,8 @@ | |||||
<None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> | <None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | |||||
<Compile Include="$(MSBuildThisFileDirectory)\IsExternalInit.cs" Link="IsExternalInit.cs" /> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@@ -0,0 +1,4 @@ | |||||
namespace System.Runtime.CompilerServices | |||||
{ | |||||
internal static class IsExternalInit { } | |||||
} |
@@ -4,4 +4,8 @@ | |||||
<TargetFramework>netstandard2.0</TargetFramework> | <TargetFramework>netstandard2.0</TargetFramework> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | |||||
<None Include="Discord.Net.SourceGenerators.Serialization.props" Pack="true" PackagePath="build" Visible="false" /> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@@ -0,0 +1,6 @@ | |||||
<Project> | |||||
<ItemGroup> | |||||
<CompilerVisibleProperty Include="DiscordNet_SerializationGenerator_OptionsTypeNamespace" /> | |||||
<CompilerVisibleProperty Include="DiscordNet_SerializationGenerator_SearchThroughReferencedAssemblies" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -1,37 +0,0 @@ | |||||
using Microsoft.CodeAnalysis; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
public partial class SerializationSourceGenerator | |||||
{ | |||||
private static string GenerateConverter(INamedTypeSymbol @class) | |||||
{ | |||||
return $@" | |||||
using System; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
namespace Discord.Net.Serialization.Converters | |||||
{{ | |||||
public class {@class.Name}Converter : JsonConverter<{@class.ToDisplayString()}> | |||||
{{ | |||||
public override {@class.ToDisplayString()} Read( | |||||
ref Utf8JsonReader reader, | |||||
Type typeToConvert, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
return default; | |||||
}} | |||||
public override void Write( | |||||
Utf8JsonWriter writer, | |||||
{@class.ToDisplayString()} value, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
writer.WriteNull(); | |||||
}} | |||||
}} | |||||
}}"; | |||||
} | |||||
} | |||||
} |
@@ -5,20 +5,26 @@ namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | { | ||||
public partial class SerializationSourceGenerator | public partial class SerializationSourceGenerator | ||||
{ | { | ||||
private static string GenerateSerializerOptionsTemplateSourceCode() | |||||
private static string GenerateSerializerOptionsSourceCode( | |||||
string @namespace, | |||||
IEnumerable<SerializedType> converters) | |||||
{ | { | ||||
return @" | |||||
using System; | |||||
var snippets = string.Join("\n", | |||||
converters.Select( | |||||
x => $" options.Converters.Add(new {@namespace}.Internal.Converters.{x.ConverterTypeName}());")); | |||||
return $@"using System; | |||||
using System.Text.Json; | using System.Text.Json; | ||||
using Discord.Net.Serialization.Converters; | |||||
namespace Discord.Net.Serialization | |||||
{ | |||||
namespace {@namespace} | |||||
{{ | |||||
/// <summary> | /// <summary> | ||||
/// Defines extension methods for adding Discord.Net JSON converters to a | /// Defines extension methods for adding Discord.Net JSON converters to a | ||||
/// <see cref=""JsonSerializerOptions""/> instance. | /// <see cref=""JsonSerializerOptions""/> instance. | ||||
/// </summary> | /// </summary> | ||||
public static partial class JsonSerializerOptionsExtensions | |||||
{ | |||||
public static class JsonSerializerOptionsExtensions | |||||
{{ | |||||
/// <summary> | /// <summary> | ||||
/// Adds Discord.Net JSON converters to the passed | /// Adds Discord.Net JSON converters to the passed | ||||
/// <see cref=""JsonSerializerOptions""/>. | /// <see cref=""JsonSerializerOptions""/>. | ||||
@@ -30,33 +36,11 @@ namespace Discord.Net.Serialization | |||||
/// The modified <see cref=""JsonSerializerOptions""/>, so this method | /// The modified <see cref=""JsonSerializerOptions""/>, so this method | ||||
/// can be chained. | /// can be chained. | ||||
/// </returns> | /// </returns> | ||||
public static partial JsonSerializerOptions WithDiscordNetConverters( | |||||
this JsonSerializerOptions options); | |||||
} | |||||
}"; | |||||
} | |||||
private static string GenerateSerializerOptionsSourceCode( | |||||
List<string> converters) | |||||
{ | |||||
var snippets = string.Join("\n", | |||||
converters.Select( | |||||
x => $"options.Converters.Add(new {x}());")); | |||||
return $@" | |||||
using System; | |||||
using System.Text.Json; | |||||
using Discord.Net.Serialization.Converters; | |||||
namespace Discord.Net.Serialization | |||||
{{ | |||||
public static partial class JsonSerializerOptionsExtensions | |||||
{{ | |||||
public static partial JsonSerializerOptions WithDiscordNetConverters( | |||||
public static JsonSerializerOptions WithDiscordNetConverters( | |||||
this JsonSerializerOptions options) | this JsonSerializerOptions options) | ||||
{{ | {{ | ||||
options.Converters.Add(new OptionalConverterFactory()); | options.Converters.Add(new OptionalConverterFactory()); | ||||
{snippets} | |||||
{snippets} | |||||
return options; | return options; | ||||
}} | }} | ||||
@@ -3,7 +3,7 @@ using System.Collections; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Reflection; | |||||
using System.Threading; | |||||
using Microsoft.CodeAnalysis; | using Microsoft.CodeAnalysis; | ||||
using Microsoft.CodeAnalysis.CSharp.Syntax; | using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
@@ -14,94 +14,136 @@ namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | { | ||||
public void Execute(GeneratorExecutionContext context) | public void Execute(GeneratorExecutionContext context) | ||||
{ | { | ||||
if (!context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( | |||||
"build_property.DiscordNet_SerializationGenerator_OptionsTypeNamespace", | |||||
out var serializerOptionsNamespace)) | |||||
throw new InvalidOperationException( | |||||
"Missing output namespace. Set DiscordNet_SerializationGenerator_OptionsTypeNamespace in your project file."); | |||||
bool searchThroughReferencedAssemblies = | |||||
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue( | |||||
"build_property.DiscordNet_SerializationGenerator_SearchThroughReferencedAssemblies", | |||||
out var _); | |||||
var generateSerializerAttribute = context.Compilation | |||||
.GetTypeByMetadataName( | |||||
"Discord.Net.Serialization.GenerateSerializerAttribute"); | |||||
var discriminatedUnionSymbol = context.Compilation | |||||
.GetTypeByMetadataName( | |||||
"Discord.Net.Serialization.DiscriminatedUnionAttribute"); | |||||
var discriminatedUnionMemberSymbol = context.Compilation | |||||
.GetTypeByMetadataName( | |||||
"Discord.Net.Serialization.DiscriminatedUnionMemberAttribute"); | |||||
Debug.Assert(generateSerializerAttribute != null); | |||||
Debug.Assert(discriminatedUnionSymbol != null); | |||||
Debug.Assert(discriminatedUnionMemberSymbol != null); | |||||
Debug.Assert(context.SyntaxContextReceiver != null); | |||||
var receiver = (SyntaxReceiver)context.SyntaxContextReceiver!; | var receiver = (SyntaxReceiver)context.SyntaxContextReceiver!; | ||||
var converters = new List<string>(); | |||||
var symbolsToBuild = receiver.GetSerializedTypes( | |||||
context.Compilation); | |||||
foreach (var @class in receiver.Classes) | |||||
if (searchThroughReferencedAssemblies) | |||||
{ | { | ||||
var semanticModel = context.Compilation.GetSemanticModel( | |||||
@class.SyntaxTree); | |||||
var visitor = new VisibleTypeVisitor(context.CancellationToken); | |||||
foreach (var module in context.Compilation.Assembly.Modules) | |||||
foreach (var reference in module.ReferencedAssemblySymbols) | |||||
visitor.Visit(reference); | |||||
if (semanticModel.GetDeclaredSymbol(@class) is | |||||
not INamedTypeSymbol classSymbol) | |||||
throw new InvalidOperationException( | |||||
"Could not find named type symbol for " + | |||||
$"{@class.Identifier}"); | |||||
symbolsToBuild = symbolsToBuild | |||||
.Concat(visitor.GetVisibleTypes()); | |||||
} | |||||
context.AddSource( | |||||
$"Converters.{classSymbol.Name}", | |||||
GenerateConverter(classSymbol)); | |||||
var types = SerializedTypeUtils.BuildTypeTrees( | |||||
generateSerializerAttribute: generateSerializerAttribute!, | |||||
discriminatedUnionSymbol: discriminatedUnionSymbol!, | |||||
discriminatedUnionMemberSymbol: discriminatedUnionMemberSymbol!, | |||||
symbolsToBuild: symbolsToBuild); | |||||
converters.Add($"{classSymbol.Name}Converter"); | |||||
foreach (var type in types) | |||||
{ | |||||
context.AddSource($"Converters.{type.ConverterTypeName}", | |||||
type.GenerateSourceCode(serializerOptionsNamespace)); | |||||
if (type is DiscriminatedUnionSerializedType duDeclaration) | |||||
foreach (var member in duDeclaration.Members) | |||||
context.AddSource( | |||||
$"Converters.{type.ConverterTypeName}.{member.ConverterTypeName}", | |||||
member.GenerateSourceCode(serializerOptionsNamespace)); | |||||
} | } | ||||
context.AddSource("SerializerOptions.Complete", | |||||
GenerateSerializerOptionsSourceCode(converters)); | |||||
context.AddSource("SerializerOptions", | |||||
GenerateSerializerOptionsSourceCode( | |||||
serializerOptionsNamespace, types)); | |||||
} | } | ||||
public void Initialize(GeneratorInitializationContext context) | public void Initialize(GeneratorInitializationContext context) | ||||
{ | |||||
context.RegisterForPostInitialization(PostInitialize); | |||||
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); | |||||
} | |||||
public static void PostInitialize( | |||||
GeneratorPostInitializationContext context) | |||||
=> context.AddSource("SerializerOptions.Template", | |||||
GenerateSerializerOptionsTemplateSourceCode()); | |||||
=> context.RegisterForSyntaxNotifications( | |||||
() => new SyntaxReceiver()); | |||||
internal class SyntaxReceiver : ISyntaxContextReceiver | |||||
private class SyntaxReceiver : ISyntaxContextReceiver | |||||
{ | { | ||||
public List<ClassDeclarationSyntax> Classes { get; } = new(); | |||||
private readonly Dictionary<string, INamedTypeSymbol> _interestingAttributes | |||||
= new(); | |||||
private readonly List<SyntaxNode> _classes; | |||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) | |||||
public SyntaxReceiver() | |||||
{ | { | ||||
_ = GetOrAddAttribute(_interestingAttributes, | |||||
context.SemanticModel, | |||||
"Discord.Net.Serialization.DiscriminatedUnionAttribute"); | |||||
_ = GetOrAddAttribute(_interestingAttributes, | |||||
context.SemanticModel, | |||||
"Discord.Net.Serialization.DiscriminatedUnionMemberAttribute"); | |||||
_classes = new(); | |||||
} | |||||
if (context.Node is ClassDeclarationSyntax classDecl | |||||
&& classDecl.AttributeLists is | |||||
SyntaxList<AttributeListSyntax> attrList | |||||
&& attrList.Any( | |||||
list => list.Attributes | |||||
.Any(a => IsInterestingAttribute(a, | |||||
context.SemanticModel, | |||||
_interestingAttributes.Values)))) | |||||
public IEnumerable<INamedTypeSymbol> GetSerializedTypes( | |||||
Compilation compilation) | |||||
{ | |||||
foreach (var @class in _classes) | |||||
{ | { | ||||
Classes.Add(classDecl); | |||||
var semanticModel = compilation.GetSemanticModel( | |||||
@class.SyntaxTree); | |||||
if (semanticModel.GetDeclaredSymbol(@class) is | |||||
INamedTypeSymbol classSymbol) | |||||
yield return classSymbol; | |||||
} | } | ||||
} | } | ||||
private static INamedTypeSymbol GetOrAddAttribute( | |||||
Dictionary<string, INamedTypeSymbol> cache, | |||||
SemanticModel model, string name) | |||||
private INamedTypeSymbol? _generateSerializerAttributeSymbol; | |||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) | |||||
{ | { | ||||
if (!cache.TryGetValue(name, out var type)) | |||||
_generateSerializerAttributeSymbol ??= | |||||
context.SemanticModel.Compilation.GetTypeByMetadataName( | |||||
"Discord.Net.Serialization.GenerateSerializerAttribute"); | |||||
Debug.Assert(_generateSerializerAttributeSymbol != null); | |||||
if (context.Node is ClassDeclarationSyntax classDeclaration | |||||
&& classDeclaration.AttributeLists is | |||||
SyntaxList<AttributeListSyntax> classAttributeLists | |||||
&& classAttributeLists.Any( | |||||
list => list.Attributes.Any( | |||||
n => IsAttribute(n, context.SemanticModel, | |||||
_generateSerializerAttributeSymbol!)))) | |||||
{ | { | ||||
type = model.Compilation.GetTypeByMetadataName(name); | |||||
Debug.Assert(type != null); | |||||
cache.Add(name, type!); | |||||
_classes.Add(classDeclaration); | |||||
} | |||||
else if (context.Node is RecordDeclarationSyntax recordDeclaration | |||||
&& recordDeclaration.AttributeLists is | |||||
SyntaxList<AttributeListSyntax> recordAttributeLists | |||||
&& recordAttributeLists.Any( | |||||
list => list.Attributes.Any( | |||||
n => IsAttribute(n, context.SemanticModel, | |||||
_generateSerializerAttributeSymbol!)))) | |||||
{ | |||||
_classes.Add(recordDeclaration); | |||||
} | } | ||||
return type!; | |||||
} | |||||
private static bool IsInterestingAttribute( | |||||
AttributeSyntax attribute, SemanticModel model, | |||||
IEnumerable<INamedTypeSymbol> interestingAttributes) | |||||
{ | |||||
var typeInfo = model.GetTypeInfo(attribute.Name); | |||||
static bool IsAttribute(AttributeSyntax attribute, | |||||
SemanticModel model, INamedTypeSymbol expected) | |||||
{ | |||||
var typeInfo = model.GetTypeInfo(attribute.Name); | |||||
return interestingAttributes.Any( | |||||
x => SymbolEqualityComparer.Default | |||||
.Equals(typeInfo.Type, x)); | |||||
return SymbolEqualityComparer.Default.Equals( | |||||
typeInfo.Type, expected); | |||||
} | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -0,0 +1,253 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using Microsoft.CodeAnalysis; | |||||
using Microsoft.CodeAnalysis.CSharp; | |||||
using static Discord.Net.SourceGenerators.Serialization.Utils; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
internal record SerializedType( | |||||
INamedTypeSymbol Declaration) | |||||
{ | |||||
public virtual string ConverterTypeName | |||||
=> $"{Declaration.Name}Converter"; | |||||
protected virtual IEnumerable<IPropertySymbol> SymbolsToSerialize | |||||
=> Declaration.GetProperties(includeInherited: true) | |||||
.Where(x => !x.IsReadOnly); | |||||
public virtual string GenerateSourceCode(string outputNamespace) | |||||
{ | |||||
var deserializers = SymbolsToSerialize | |||||
.Select(GenerateFieldReader); | |||||
var bytes = string.Join("\n", | |||||
deserializers.Select(x => x.utf8)); | |||||
var fields = string.Join("\n", | |||||
deserializers.Select(x => x.field)); | |||||
var readers = string.Join("\n", | |||||
deserializers.Select(x => x.reader)); | |||||
var fieldUnassigned = string.Join("\n || ", | |||||
deserializers | |||||
.Where(x => x.type.NullableAnnotation != NullableAnnotation.Annotated) | |||||
.Select( | |||||
x => $"{x.snakeCase}OrDefault is not {x.type} {x.snakeCase}")); | |||||
var constructorParams = string.Join(",\n", | |||||
deserializers | |||||
.Select(x => $" {x.name}: {x.snakeCase}{(x.type.NullableAnnotation == NullableAnnotation.Annotated ? "OrDefault" : "")}")); | |||||
return $@"using System; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
namespace {outputNamespace}.Converters | |||||
{{ | |||||
internal class {ConverterTypeName} : JsonConverter<{Declaration.ToDisplayString()}> | |||||
{{ | |||||
{bytes} | |||||
public override {Declaration.ToDisplayString()}? Read( | |||||
ref Utf8JsonReader reader, | |||||
Type typeToConvert, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
if (reader.TokenType != JsonTokenType.StartObject) | |||||
throw new JsonException(""Expected StartObject""); | |||||
{fields} | |||||
while (reader.Read()) | |||||
{{ | |||||
if (reader.TokenType == JsonTokenType.EndObject) | |||||
break; | |||||
if (reader.TokenType != JsonTokenType.PropertyName) | |||||
throw new JsonException(""Expected PropertyName""); | |||||
{readers} | |||||
else if (!reader.Read()) | |||||
throw new JsonException(); | |||||
if (reader.TokenType == JsonTokenType.StartArray | |||||
|| reader.TokenType == JsonTokenType.StartObject) | |||||
reader.Skip(); | |||||
}} | |||||
if ({fieldUnassigned}) | |||||
throw new JsonException(""Missing field""); | |||||
return new {Declaration.ToDisplayString()}( | |||||
{constructorParams} | |||||
); | |||||
}} | |||||
public override void Write( | |||||
Utf8JsonWriter writer, | |||||
{Declaration.ToDisplayString()} value, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
writer.WriteNullValue(); | |||||
}} | |||||
}} | |||||
}}"; | |||||
static (string name, ITypeSymbol type, string snakeCase, string utf8, string field, string reader) | |||||
GenerateFieldReader(IPropertySymbol member, int position) | |||||
{ | |||||
var needsNullableAnnotation = false; | |||||
if (member.Type.IsValueType | |||||
&& member.Type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T) | |||||
needsNullableAnnotation = true; | |||||
var snakeCase = ConvertToSnakeCase(member.Name); | |||||
return (member.Name, member.Type, snakeCase, | |||||
$@" private static ReadOnlySpan<byte> {member.Name}Bytes => new byte[] | |||||
{{ | |||||
// {snakeCase} | |||||
{string.Join(", ", Encoding.UTF8.GetBytes(snakeCase))} | |||||
}};", | |||||
$" {member.Type.WithNullableAnnotation(NullableAnnotation.Annotated).ToDisplayString()}{(needsNullableAnnotation ? "?" : "")} {snakeCase}OrDefault = default;", | |||||
$@" {(position > 0 ? "else " : "")}if (reader.ValueTextEquals({member.Name}Bytes)) | |||||
{{ | |||||
if (!reader.Read()) | |||||
throw new JsonException(""Expected value""); | |||||
var cvt = options.GetConverter( | |||||
typeof({member.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString()})); | |||||
if (cvt is JsonConverter<{member.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString()}> converter) | |||||
{snakeCase}OrDefault = converter.Read(ref reader, | |||||
typeof({member.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString()}), | |||||
options); | |||||
else | |||||
{snakeCase}OrDefault = JsonSerializer.Deserialize<{member.Type.ToDisplayString()}>( | |||||
ref reader, options); | |||||
}}"); | |||||
} | |||||
} | |||||
} | |||||
internal record DiscriminatedUnionSerializedType( | |||||
INamedTypeSymbol Declaration, | |||||
ISymbol Discriminator) | |||||
: SerializedType(Declaration) | |||||
{ | |||||
public List<DiscriminatedUnionMemberSerializedType> Members { get; } | |||||
= new(); | |||||
public override string GenerateSourceCode(string outputNamespace) | |||||
{ | |||||
var discriminatorField = ConvertToSnakeCase(Discriminator.Name); | |||||
var discriminatorType = Discriminator switch | |||||
{ | |||||
IPropertySymbol prop => prop.Type, | |||||
IFieldSymbol field => field.Type, | |||||
_ => throw new InvalidOperationException( | |||||
"Unsupported discriminator member type") | |||||
}; | |||||
var switchCaseMembers = string.Join(",\n", | |||||
Members.Select( | |||||
x => $@" {x.DiscriminatorValue.ToCSharpString()} | |||||
=> JsonSerializer.Deserialize(ref copy, | |||||
typeof({x.Declaration.ToDisplayString()}), options)")); | |||||
return $@"using System; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
namespace {outputNamespace}.Internal.Converters | |||||
{{ | |||||
internal class {ConverterTypeName} : JsonConverter<{Declaration.ToDisplayString()}> | |||||
{{ | |||||
private static ReadOnlySpan<byte> DiscriminatorBytes => new byte[] | |||||
{{ | |||||
// {discriminatorField} | |||||
{string.Join(", ", Encoding.UTF8.GetBytes(discriminatorField))} | |||||
}}; | |||||
public override {Declaration.ToDisplayString()}? Read( | |||||
ref Utf8JsonReader reader, | |||||
Type typeToConvert, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
var copy = reader; | |||||
if (reader.TokenType != JsonTokenType.StartObject) | |||||
throw new JsonException(""Expected StartObject""); | |||||
{discriminatorType.ToDisplayString()}? discriminator = null; | |||||
while (reader.Read()) | |||||
{{ | |||||
if (reader.TokenType == JsonTokenType.EndObject) | |||||
break; | |||||
if (reader.TokenType != JsonTokenType.PropertyName) | |||||
throw new JsonException(""Expected PropertyName""); | |||||
if (reader.ValueTextEquals(DiscriminatorBytes)) | |||||
{{ | |||||
if (!reader.Read()) | |||||
throw new JsonException(""Expected value""); | |||||
var cvt = options.GetConverter( | |||||
typeof({discriminatorType.ToDisplayString()})); | |||||
if (cvt is JsonConverter<{discriminatorType.ToDisplayString()}> converter) | |||||
discriminator = converter.Read(ref reader, | |||||
typeof({discriminatorType.ToDisplayString()}), | |||||
options); | |||||
else | |||||
discriminator = JsonSerializer | |||||
.Deserialize<{discriminatorType.ToDisplayString()}>( | |||||
ref reader, options); | |||||
}} | |||||
else if (!reader.Read()) | |||||
throw new JsonException(""Expected value""); | |||||
if (reader.TokenType == JsonTokenType.StartArray | |||||
|| reader.TokenType == JsonTokenType.StartObject) | |||||
reader.Skip(); | |||||
}} | |||||
var result = discriminator switch | |||||
{{ | |||||
{switchCaseMembers}, | |||||
_ => throw new JsonException(""Unknown discriminator value"") | |||||
}} as {Declaration.ToDisplayString()}; | |||||
reader = copy; | |||||
return result; | |||||
}} | |||||
public override void Write( | |||||
Utf8JsonWriter writer, | |||||
{Declaration.ToDisplayString()} value, | |||||
JsonSerializerOptions options) | |||||
{{ | |||||
writer.WriteNullValue(); | |||||
}} | |||||
}} | |||||
}}"; | |||||
} | |||||
} | |||||
internal record DiscriminatedUnionMemberSerializedType( | |||||
INamedTypeSymbol Declaration, | |||||
TypedConstant DiscriminatorValue) | |||||
: SerializedType(Declaration) | |||||
{ | |||||
public DiscriminatedUnionSerializedType? DiscriminatedUnionDeclaration | |||||
{ get; init; } | |||||
protected override IEnumerable<IPropertySymbol> SymbolsToSerialize | |||||
=> base.SymbolsToSerialize | |||||
.Where(x => !SymbolEqualityComparer.Default.Equals(x, | |||||
DiscriminatedUnionDeclaration?.Discriminator)); | |||||
} | |||||
} |
@@ -0,0 +1,119 @@ | |||||
using System; | |||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using Microsoft.CodeAnalysis; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
internal static class SerializedTypeUtils | |||||
{ | |||||
public static List<SerializedType> BuildTypeTrees( | |||||
INamedTypeSymbol generateSerializerAttribute, | |||||
INamedTypeSymbol discriminatedUnionSymbol, | |||||
INamedTypeSymbol discriminatedUnionMemberSymbol, | |||||
IEnumerable<INamedTypeSymbol> symbolsToBuild) | |||||
{ | |||||
var types = new List<SerializedType>(); | |||||
FindAllSerializedTypes(types, generateSerializerAttribute, | |||||
discriminatedUnionSymbol, discriminatedUnionMemberSymbol, | |||||
symbolsToBuild); | |||||
// Now, move DU members into their relevant DU declaration. | |||||
int x = 0; | |||||
while (x < types.Count) | |||||
{ | |||||
var type = types[x]; | |||||
if (type is DiscriminatedUnionMemberSerializedType duMember) | |||||
{ | |||||
var declaration = types.FirstOrDefault( | |||||
x => SymbolEqualityComparer.Default.Equals( | |||||
x.Declaration, duMember.Declaration.BaseType)); | |||||
if (declaration is not DiscriminatedUnionSerializedType duDeclaration) | |||||
throw new InvalidOperationException( | |||||
"Could not find DU declaration for DU " + | |||||
$"member {duMember.Declaration.ToDisplayString()}"); | |||||
duDeclaration.Members.Add(duMember with | |||||
{ | |||||
DiscriminatedUnionDeclaration = duDeclaration | |||||
}); | |||||
types.RemoveAt(x); | |||||
continue; | |||||
} | |||||
x++; | |||||
} | |||||
return types; | |||||
} | |||||
private static void FindAllSerializedTypes( | |||||
List<SerializedType> types, | |||||
INamedTypeSymbol generateSerializerAttribute, | |||||
INamedTypeSymbol discriminatedUnionSymbol, | |||||
INamedTypeSymbol discriminatedUnionMemberSymbol, | |||||
IEnumerable<INamedTypeSymbol> symbolsToBuild) | |||||
{ | |||||
foreach (var type in symbolsToBuild) | |||||
{ | |||||
var generateSerializer = type.GetAttributes() | |||||
.Any(x => SymbolEqualityComparer.Default | |||||
.Equals(x.AttributeClass, generateSerializerAttribute)); | |||||
if (!generateSerializer) | |||||
continue; | |||||
var duDeclaration = type.GetAttributes() | |||||
.FirstOrDefault(x => SymbolEqualityComparer.Default | |||||
.Equals(x.AttributeClass, discriminatedUnionSymbol)); | |||||
if (duDeclaration != null) | |||||
{ | |||||
if (duDeclaration | |||||
.ConstructorArguments | |||||
.FirstOrDefault() | |||||
.Value is not string memberName) | |||||
throw new InvalidOperationException( | |||||
"Failed to get DU discriminator member name"); | |||||
var member = type.GetMembers(memberName) | |||||
.FirstOrDefault( | |||||
x => x is IPropertySymbol or IFieldSymbol); | |||||
if (member is null) | |||||
throw new InvalidOperationException( | |||||
"Failed to get DU discriminator member symbol"); | |||||
types.Add(new DiscriminatedUnionSerializedType( | |||||
type, member)); | |||||
continue; | |||||
} | |||||
var duMemberDeclaration = type | |||||
.GetAttributes() | |||||
.FirstOrDefault(x => SymbolEqualityComparer.Default | |||||
.Equals(x.AttributeClass, | |||||
discriminatedUnionMemberSymbol)); | |||||
if (duMemberDeclaration != null) | |||||
{ | |||||
if (duMemberDeclaration.ConstructorArguments.Length == 0 | |||||
|| duMemberDeclaration.ConstructorArguments[0].IsNull) | |||||
throw new InvalidOperationException( | |||||
"Failed to get DU discriminator value"); | |||||
types.Add(new DiscriminatedUnionMemberSerializedType( | |||||
type, duMemberDeclaration.ConstructorArguments[0])); | |||||
continue; | |||||
} | |||||
types.Add(new SerializedType(type)); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,27 @@ | |||||
using System; | |||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics.SymbolStore; | |||||
using Microsoft.CodeAnalysis; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
internal static class SymbolExtensions | |||||
{ | |||||
public static IEnumerable<IPropertySymbol> GetProperties( | |||||
this INamedTypeSymbol symbol, | |||||
bool includeInherited) | |||||
{ | |||||
var s = symbol; | |||||
do | |||||
{ | |||||
foreach (var member in s.GetMembers()) | |||||
if (member is IPropertySymbol property) | |||||
yield return property; | |||||
s = s.BaseType; | |||||
} | |||||
while (includeInherited && s != null); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,25 @@ | |||||
using System.Text; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
internal static class Utils | |||||
{ | |||||
private static readonly StringBuilder CaseChangeBuffer = new(); | |||||
public static string ConvertToSnakeCase(string value) | |||||
{ | |||||
foreach (var c in value) | |||||
{ | |||||
if (char.IsUpper(c) && CaseChangeBuffer.Length > 0) | |||||
_ = CaseChangeBuffer.Append('_'); | |||||
_ = CaseChangeBuffer.Append(char.ToLower(c)); | |||||
} | |||||
var result = CaseChangeBuffer.ToString(); | |||||
_ = CaseChangeBuffer.Clear(); | |||||
return result; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,59 @@ | |||||
using System.Collections.Generic; | |||||
using System.Threading; | |||||
using Microsoft.CodeAnalysis; | |||||
namespace Discord.Net.SourceGenerators.Serialization | |||||
{ | |||||
internal sealed class VisibleTypeVisitor | |||||
: SymbolVisitor | |||||
{ | |||||
private readonly CancellationToken _cancellationToken; | |||||
private readonly HashSet<INamedTypeSymbol> _typeSymbols; | |||||
public VisibleTypeVisitor(CancellationToken cancellationToken) | |||||
{ | |||||
_cancellationToken = cancellationToken; | |||||
_typeSymbols = new(SymbolEqualityComparer.Default); | |||||
} | |||||
public IEnumerable<INamedTypeSymbol> GetVisibleTypes() | |||||
=> _typeSymbols; | |||||
public override void VisitAssembly(IAssemblySymbol symbol) | |||||
{ | |||||
_cancellationToken.ThrowIfCancellationRequested(); | |||||
symbol.GlobalNamespace.Accept(this); | |||||
} | |||||
public override void VisitNamespace(INamespaceSymbol symbol) | |||||
{ | |||||
foreach (var member in symbol.GetMembers()) | |||||
{ | |||||
_cancellationToken.ThrowIfCancellationRequested(); | |||||
member.Accept(this); | |||||
} | |||||
} | |||||
public override void VisitNamedType(INamedTypeSymbol symbol) | |||||
{ | |||||
_cancellationToken.ThrowIfCancellationRequested(); | |||||
var isVisible = symbol.DeclaredAccessibility switch | |||||
{ | |||||
Accessibility.Protected => true, | |||||
Accessibility.ProtectedOrInternal => true, | |||||
Accessibility.Public => true, | |||||
_ => false, | |||||
}; | |||||
if (!isVisible || !_typeSymbols.Add(symbol)) | |||||
return; | |||||
foreach (var member in symbol.GetTypeMembers()) | |||||
{ | |||||
_cancellationToken.ThrowIfCancellationRequested(); | |||||
member.Accept(this); | |||||
} | |||||
} | |||||
} | |||||
} |