mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-26 09:33:09 -04:00
286 lines
12 KiB
C#
286 lines
12 KiB
C#
namespace SpacetimeDB;
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
static class Utils
|
|
{
|
|
internal static string SymbolToName(ISymbol symbol)
|
|
{
|
|
return symbol.ToDisplayString(
|
|
SymbolDisplayFormat
|
|
.FullyQualifiedFormat
|
|
.WithMemberOptions(SymbolDisplayMemberOptions.IncludeContainingType)
|
|
.WithGenericsOptions(SymbolDisplayGenericsOptions.IncludeTypeParameters)
|
|
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
|
|
);
|
|
}
|
|
|
|
internal static void RegisterSourceOutputs(
|
|
this IncrementalValuesProvider<KeyValuePair<string, string>> methods,
|
|
IncrementalGeneratorInitializationContext context
|
|
)
|
|
{
|
|
context.RegisterSourceOutput(
|
|
methods,
|
|
(context, method) =>
|
|
{
|
|
context.AddSource(
|
|
$"{string.Join("_", method.Key.Split(System.IO.Path.GetInvalidFileNameChars()))}.cs",
|
|
$@"
|
|
// <auto-generated />
|
|
#nullable enable
|
|
|
|
{method.Value}
|
|
"
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
public static string GetTypeInfo(ITypeSymbol type)
|
|
{
|
|
// We need to distinguish handle nullable reference types specially:
|
|
// compiler expands something like `int?` to `System.Nullable<int>` but with the nullable annotation set to `Annotated`
|
|
// while something like `string?` is expanded to `string` with the nullable annotation set to `Annotated`...
|
|
// Beautiful design requires beautiful hacks.
|
|
if (
|
|
type.NullableAnnotation == NullableAnnotation.Annotated
|
|
&& type.OriginalDefinition.ToString() != "System.Nullable<T>"
|
|
)
|
|
{
|
|
// if we're here, then this is a nullable reference type like `string?`.
|
|
return $"SpacetimeDB.SATS.SumType.MakeRefOption({GetTypeInfo(type.WithNullableAnnotation(NullableAnnotation.None))})";
|
|
}
|
|
return type switch
|
|
{
|
|
ITypeParameterSymbol typeParameter => $"{typeParameter.Name}TypeInfo",
|
|
INamedTypeSymbol namedType
|
|
=> type.SpecialType switch
|
|
{
|
|
SpecialType.System_Boolean => "SpacetimeDB.SATS.BuiltinType.BoolTypeInfo",
|
|
SpecialType.System_SByte => "SpacetimeDB.SATS.BuiltinType.I8TypeInfo",
|
|
SpecialType.System_Byte => "SpacetimeDB.SATS.BuiltinType.U8TypeInfo",
|
|
SpecialType.System_Int16 => "SpacetimeDB.SATS.BuiltinType.I16TypeInfo",
|
|
SpecialType.System_UInt16 => "SpacetimeDB.SATS.BuiltinType.U16TypeInfo",
|
|
SpecialType.System_Int32 => "SpacetimeDB.SATS.BuiltinType.I32TypeInfo",
|
|
SpecialType.System_UInt32 => "SpacetimeDB.SATS.BuiltinType.U32TypeInfo",
|
|
SpecialType.System_Int64 => "SpacetimeDB.SATS.BuiltinType.I64TypeInfo",
|
|
SpecialType.System_UInt64 => "SpacetimeDB.SATS.BuiltinType.U64TypeInfo",
|
|
SpecialType.System_Single => "SpacetimeDB.SATS.BuiltinType.F32TypeInfo",
|
|
SpecialType.System_Double => "SpacetimeDB.SATS.BuiltinType.F64TypeInfo",
|
|
SpecialType.System_String => "SpacetimeDB.SATS.BuiltinType.StringTypeInfo",
|
|
SpecialType.None when type.ToString() == "System.Int128"
|
|
=> "SpacetimeDB.SATS.BuiltinType.I128TypeInfo",
|
|
SpecialType.None when type.ToString() == "System.UInt128"
|
|
=> "SpacetimeDB.SATS.BuiltinType.U128TypeInfo",
|
|
SpecialType.None
|
|
when namedType.EnumUnderlyingType is not null
|
|
// check that enums also have [SpacetimeDB.Type]
|
|
// we don't currently do anything special whether or not it exists but might in the future
|
|
// so this requirement is mostly for future-proofing
|
|
&& type.GetAttributes()
|
|
.Any(
|
|
a =>
|
|
a.AttributeClass?.ToDisplayString()
|
|
== "SpacetimeDB.TypeAttribute"
|
|
)
|
|
=> $"SpacetimeDB.SATS.BuiltinType.MakeEnum<{type}>()",
|
|
SpecialType.None
|
|
=> $"{type.OriginalDefinition.ToString() switch
|
|
{
|
|
"System.Collections.Generic.List<T>" => "SpacetimeDB.SATS.BuiltinType.MakeList",
|
|
"System.Collections.Generic.Dictionary<TKey, TValue>" => "SpacetimeDB.SATS.BuiltinType.MakeMap",
|
|
// If we're here, then this is nullable value type like `int?`.
|
|
"System.Nullable<T>" => $"SpacetimeDB.SATS.SumType.MakeValueOption",
|
|
var name when name.StartsWith("System.") => throw new InvalidOperationException(
|
|
$"Unsupported system type {name}"
|
|
),
|
|
_ => $"{type}.GetSatsTypeInfo",
|
|
}}({string.Join(", ", namedType.TypeArguments.Select(GetTypeInfo))})",
|
|
_
|
|
=> throw new InvalidOperationException(
|
|
$"Unsupported special type {type.SpecialType} ({type})"
|
|
)
|
|
},
|
|
IArrayTypeSymbol arrayType
|
|
=> arrayType.ElementType is INamedTypeSymbol namedType
|
|
&& namedType.SpecialType == SpecialType.System_Byte
|
|
? "SpacetimeDB.SATS.BuiltinType.BytesTypeInfo"
|
|
: $"SpacetimeDB.SATS.BuiltinType.MakeArray({GetTypeInfo(arrayType.ElementType)})",
|
|
_ => throw new InvalidOperationException($"Unsupported type {type}")
|
|
};
|
|
}
|
|
|
|
// Borrowed & modified code for generating in-place extensions for partial structs/classes/etc. Source:
|
|
// https://andrewlock.net/creating-a-source-generator-part-5-finding-a-type-declarations-namespace-and-type-hierarchy/
|
|
|
|
public class Scope
|
|
{
|
|
private string nameSpace;
|
|
private ParentClass? parentClasses;
|
|
|
|
public Scope(TypeDeclarationSyntax type)
|
|
{
|
|
nameSpace = GetNamespace(type);
|
|
parentClasses = GetParentClasses(type);
|
|
}
|
|
|
|
// determine the namespace the class/enum/struct is declared in, if any
|
|
static string GetNamespace(BaseTypeDeclarationSyntax syntax)
|
|
{
|
|
// If we don't have a namespace at all we'll return an empty string
|
|
// This accounts for the "default namespace" case
|
|
string nameSpace = string.Empty;
|
|
|
|
// Get the containing syntax node for the type declaration
|
|
// (could be a nested type, for example)
|
|
SyntaxNode? potentialNamespaceParent = syntax.Parent;
|
|
|
|
// Keep moving "out" of nested classes etc until we get to a namespace
|
|
// or until we run out of parents
|
|
while (
|
|
potentialNamespaceParent != null
|
|
&& potentialNamespaceParent is not NamespaceDeclarationSyntax
|
|
&& potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax
|
|
)
|
|
{
|
|
potentialNamespaceParent = potentialNamespaceParent.Parent;
|
|
}
|
|
|
|
// Build up the final namespace by looping until we no longer have a namespace declaration
|
|
if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
|
|
{
|
|
// We have a namespace. Use that as the type
|
|
nameSpace = namespaceParent.Name.ToString();
|
|
|
|
// Keep moving "out" of the namespace declarations until we
|
|
// run out of nested namespace declarations
|
|
while (true)
|
|
{
|
|
if (namespaceParent.Parent is not NamespaceDeclarationSyntax parent)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Add the outer namespace as a prefix to the final namespace
|
|
nameSpace = $"{namespaceParent.Name}.{nameSpace}";
|
|
namespaceParent = parent;
|
|
}
|
|
}
|
|
|
|
// return the final namespace
|
|
return nameSpace;
|
|
}
|
|
|
|
public class ParentClass
|
|
{
|
|
public ParentClass(string keyword, string name, string constraints, ParentClass? child)
|
|
{
|
|
Keyword = keyword;
|
|
Name = name;
|
|
Constraints = constraints;
|
|
Child = child;
|
|
}
|
|
|
|
public ParentClass? Child { get; }
|
|
public string Keyword { get; }
|
|
public string Name { get; }
|
|
public string Constraints { get; }
|
|
}
|
|
|
|
static ParentClass? GetParentClasses(TypeDeclarationSyntax typeSyntax)
|
|
{
|
|
// Try and get the parent syntax. If it isn't a type like class/struct, this will be null
|
|
TypeDeclarationSyntax? parentSyntax = typeSyntax;
|
|
ParentClass? parentClassInfo = null;
|
|
|
|
// Keep looping while we're in a supported nested type
|
|
while (parentSyntax != null && IsAllowedKind(parentSyntax.Kind()))
|
|
{
|
|
// Record the parent type keyword (class/struct etc), name, and constraints
|
|
parentClassInfo = new ParentClass(
|
|
keyword: parentSyntax.Keyword.ValueText,
|
|
name: parentSyntax.Identifier.ToString() + parentSyntax.TypeParameterList,
|
|
constraints: parentSyntax.ConstraintClauses.ToString(),
|
|
child: parentClassInfo
|
|
); // set the child link (null initially)
|
|
|
|
// Move to the next outer type
|
|
parentSyntax = (parentSyntax.Parent as TypeDeclarationSyntax);
|
|
}
|
|
|
|
// return a link to the outermost parent type
|
|
return parentClassInfo;
|
|
}
|
|
|
|
// We can only be nested in class/struct/record
|
|
static bool IsAllowedKind(SyntaxKind kind) =>
|
|
kind == SyntaxKind.ClassDeclaration
|
|
|| kind == SyntaxKind.StructDeclaration
|
|
|| kind == SyntaxKind.RecordDeclaration;
|
|
|
|
public string GenerateExtensions(string contents)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
// If we don't have a namespace, generate the code in the "default"
|
|
// namespace, either global:: or a different <RootNamespace>
|
|
var hasNamespace = !string.IsNullOrEmpty(nameSpace);
|
|
if (hasNamespace)
|
|
{
|
|
// We could use a file-scoped namespace here which would be a little impler,
|
|
// but that requires C# 10, which might not be available.
|
|
// Depends what you want to support!
|
|
sb.Append("namespace ")
|
|
.Append(nameSpace)
|
|
.AppendLine(
|
|
@"
|
|
{"
|
|
);
|
|
}
|
|
|
|
// Loop through the full parent type hiearchy, starting with the outermost
|
|
var parentsCount = 0;
|
|
while (parentClasses is not null)
|
|
{
|
|
sb.Append(" partial ")
|
|
.Append(parentClasses.Keyword) // e.g. class/struct/record
|
|
.Append(' ')
|
|
.Append(parentClasses.Name) // e.g. Outer/Generic<T>
|
|
.Append(' ')
|
|
.Append(parentClasses.Constraints) // e.g. where T: new()
|
|
.AppendLine(
|
|
@"
|
|
{"
|
|
);
|
|
parentsCount++; // keep track of how many layers deep we are
|
|
parentClasses = parentClasses.Child; // repeat with the next child
|
|
}
|
|
|
|
// Write the actual target generation code here. Not shown for brevity
|
|
sb.AppendLine(contents);
|
|
|
|
// We need to "close" each of the parent types, so write
|
|
// the required number of '}'
|
|
for (int i = 0; i < parentsCount; i++)
|
|
{
|
|
sb.AppendLine(@" }");
|
|
}
|
|
|
|
// Close the namespace, if we had one
|
|
if (hasNamespace)
|
|
{
|
|
sb.Append('}').AppendLine();
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
}
|