Files
Viktor Szépe f6da9e1f5f Fix typos (#2812)
Signed-off-by: Viktor Szépe <viktor@szepe.net>
2025-06-04 16:33:32 +00:00

374 lines
15 KiB
C#

namespace SpacetimeDB.Codegen;
using System.Collections;
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static System.Collections.StructuralComparisons;
public static class Utils
{
// Even `ImmutableArray<T>` is not deeply equatable, which makes it a common
// pain point for source generators as they must use only cacheable types.
// As a result, everyone builds their own `EquatableArray<T>` type.
public readonly record struct EquatableArray<T>(ImmutableArray<T> Array) : IEnumerable<T>
where T : IEquatable<T>
{
public int Length => Array.Length;
public T this[int index] => Array[index];
public bool Equals(EquatableArray<T> other) => Array.SequenceEqual(other.Array);
public override int GetHashCode() => StructuralEqualityComparer.GetHashCode(Array);
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>)Array).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator();
}
private static readonly SymbolDisplayFormat SymbolFormat = SymbolDisplayFormat
.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
.AddMemberOptions(SymbolDisplayMemberOptions.IncludeContainingType)
.AddMiscellaneousOptions(
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier
);
public static string SymbolToName(ISymbol symbol)
{
return symbol.ToDisplayString(SymbolFormat);
}
public static void RegisterSourceOutputs(
this IncrementalValuesProvider<Scope.Extensions> methods,
IncrementalGeneratorInitializationContext context
)
{
context.RegisterSourceOutput(
methods,
(context, method) =>
{
// Unfortunately, Roslyn doesn't expose its list of valid hintName characters
// (https://github.com/dotnet/roslyn/blob/a69841b8ca9751bee0fe9fdeedc705e198e195d9/src/Compilers/Core/Portable/SourceGeneration/AdditionalSourcesCollection.cs#L43-L66)
// but it does complain if you try to use invalid one. Let's do a conservative cleanup.
var name = string.Concat(
method.FullName.Select(c =>
SyntaxFacts.IsIdentifierPartCharacter(c) || c == '.' ? c : '_'
)
);
context.AddSource(
$"{name}.cs",
$"""
// <auto-generated />
#nullable enable
{method}
"""
);
}
);
}
public static string MakeRwTypeParam(string typeParam) => typeParam + "RW";
public class UnresolvedTypeException(INamedTypeSymbol type)
: InvalidOperationException($"Could not resolve type {type}") { }
/// <summary>
/// Return whether a type is a nullable, non-value type.
/// For example, `string?`.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsNullableReferenceType(ITypeSymbol type) =>
// We need to distinguish handle nullable reference types specially:
// compiler expands something like `int?` to `System.Nullable<int>` with the nullable annotation set to `Annotated`
// while something like `string?` is expanded to `string` with the nullable annotation set to `Annotated`.
type.NullableAnnotation == NullableAnnotation.Annotated
&& type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T;
/// <summary>
/// Get the BSATN struct name for a type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="UnresolvedTypeException"></exception>
public static string GetTypeInfo(ITypeSymbol type)
{
if (IsNullableReferenceType(type))
{
// If we're here, then this is a nullable reference type like `string?` and the original definition is `string`.
type = type.WithNullableAnnotation(NullableAnnotation.None);
return $"SpacetimeDB.BSATN.RefOption<{type}, {GetTypeInfo(type)}>";
}
return type switch
{
ITypeParameterSymbol typeParameter => MakeRwTypeParam(typeParameter.Name),
INamedTypeSymbol namedType => type.SpecialType switch
{
SpecialType.System_Boolean => "SpacetimeDB.BSATN.Bool",
SpecialType.System_SByte => "SpacetimeDB.BSATN.I8",
SpecialType.System_Byte => "SpacetimeDB.BSATN.U8",
SpecialType.System_Int16 => "SpacetimeDB.BSATN.I16",
SpecialType.System_UInt16 => "SpacetimeDB.BSATN.U16",
SpecialType.System_Int32 => "SpacetimeDB.BSATN.I32",
SpecialType.System_UInt32 => "SpacetimeDB.BSATN.U32",
SpecialType.System_Int64 => "SpacetimeDB.BSATN.I64",
SpecialType.System_UInt64 => "SpacetimeDB.BSATN.U64",
SpecialType.System_Single => "SpacetimeDB.BSATN.F32",
SpecialType.System_Double => "SpacetimeDB.BSATN.F64",
SpecialType.System_String => "SpacetimeDB.BSATN.String",
SpecialType.None => GetTypeInfoForNamedType(namedType),
_ => throw new InvalidOperationException(
$"Unsupported special type {type} ({type.SpecialType})"
),
},
IArrayTypeSymbol { ElementType: var elementType } => elementType.SpecialType
== SpecialType.System_Byte
? "SpacetimeDB.BSATN.ByteArray"
: $"SpacetimeDB.BSATN.Array<{elementType}, {GetTypeInfo(elementType)}>",
_ => throw new InvalidOperationException($"Unsupported type {type}"),
};
static string GetTypeInfoForNamedType(INamedTypeSymbol type)
{
if (type.TypeKind == Microsoft.CodeAnalysis.TypeKind.Error)
{
throw new UnresolvedTypeException(type);
}
if (type.TypeKind == Microsoft.CodeAnalysis.TypeKind.Enum)
{
if (
!type.GetAttributes()
.Any(a => a.AttributeClass?.ToString() == "SpacetimeDB.TypeAttribute")
)
{
throw new InvalidOperationException(
$"Enum {type} does not have a [SpacetimeDB.Type] attribute"
);
}
return $"SpacetimeDB.BSATN.Enum<{SymbolToName(type)}>";
}
var result = type.OriginalDefinition.ToString() switch
{
// {U/I}{128/256} are not treated by C# as regular primitives, so we need to match them by type name.
"System.Int128" => "SpacetimeDB.BSATN.I128",
"System.UInt128" => "SpacetimeDB.BSATN.U128",
"SpacetimeDB.I128" => "SpacetimeDB.BSATN.I128Stdb",
"SpacetimeDB.U128" => "SpacetimeDB.BSATN.U128Stdb",
"SpacetimeDB.I256" => "SpacetimeDB.BSATN.I256",
"SpacetimeDB.U256" => "SpacetimeDB.BSATN.U256",
"System.Collections.Generic.List<T>" => $"SpacetimeDB.BSATN.List",
// If we're here, then this is nullable *value* type like `int?`.
"System.Nullable<T>" => $"SpacetimeDB.BSATN.ValueOption",
var name when name.StartsWith("System.") => throw new InvalidOperationException(
$"Unsupported system type {name}"
),
_ => $"{SymbolToName(type)}.BSATN",
};
if (type.IsGenericType)
{
result =
$"{result}<{string.Join(", ", type.TypeArguments.Select(SymbolToName).Concat(type.TypeArguments.Select(GetTypeInfo)))}>";
}
return result;
}
}
// Polyfill for .NET methods from .NET Standard 2.1+:
private static StringBuilder AppendJoin<T>(
this StringBuilder sb,
string separator,
IEnumerable<T> values
)
{
var first = true;
foreach (var value in values)
{
if (!first)
{
sb.Append(separator);
}
first = false;
sb.Append(value);
}
return sb;
}
private static object? ResolveConstant(TypedConstant constant, System.Type targetType)
{
if (constant.Kind == TypedConstantKind.Array)
{
// We can't use LINQ ToArray() here because it doesn't support dynamic Type
// and will build `object[]` instead of the desired `T[]`.
var elementType = targetType.GetElementType();
var array = Array.CreateInstance(elementType, constant.Values.Length);
for (var i = 0; i < constant.Values.Length; i++)
{
array.SetValue(ResolveConstant(constant.Values[i], elementType), i);
}
return array;
}
return constant.Value;
}
public static T ParseAs<T>(this AttributeData attrData, System.Type? type = null)
where T : Attribute
{
type ??= typeof(T);
// For now only support attributes with a single constructor.
//
// Proper overload resolution is complicated due to implicit casts
// (in particular, enums are represented as integers in the attribute data),
// which prevent APIs like `Activator.CreateInstance` from finding the constructor.
//
// Expand logic in the future if it ever becomes actually necessary.
var ctor = type.GetConstructors().Single();
var ctorArgs = attrData
.ConstructorArguments.Zip(
ctor.GetParameters().Select(param => param.ParameterType),
ResolveConstant
)
.ToArray();
var attr = (T)ctor.Invoke(ctorArgs);
foreach (var arg in attrData.NamedArguments)
{
var prop = type.GetProperty(arg.Key);
prop.SetValue(attr, ResolveConstant(arg.Value, prop.PropertyType));
}
return attr;
}
// 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 readonly record struct Scope
{
// Reversed list of typescopes, from innermost to outermost.
private readonly EquatableArray<TypeScope> typeScopes;
// Reversed list of namespaces, from innermost to outermost.
private readonly EquatableArray<string> namespaces;
public Scope(MemberDeclarationSyntax? node)
{
var typeScopes_ = ImmutableArray.CreateBuilder<TypeScope>();
// Keep looping while we're in a supported nested type
while (node is TypeDeclarationSyntax type)
{
// Record the parent type keyword (class/struct etc), name, and constraints
typeScopes_.Add(
new TypeScope(
Keyword: type.Keyword.ValueText,
Name: type.Identifier.ToString() + type.TypeParameterList,
Constraints: type.ConstraintClauses.ToString()
)
); // set the child link (null initially)
// Move to the next outer type
node = type.Parent as MemberDeclarationSyntax;
}
typeScopes = new(typeScopes_.ToImmutable());
// We've now reached the outermost type, so we can determine the namespace
var namespaces_ = ImmutableArray.CreateBuilder<string>();
while (node is BaseNamespaceDeclarationSyntax ns)
{
namespaces_.Add(ns.Name.ToString());
node = node.Parent as MemberDeclarationSyntax;
}
namespaces = new(namespaces_.ToImmutable());
}
/// <returns>Whether this Scope is a struct declaration.</returns>
public bool IsStruct
{
get => typeScopes[0].Keyword == "struct";
}
/// <returns>Whether this Scope is a record declaration.</returns>
public bool IsRecord
{
get => typeScopes[0].Keyword == "record";
}
public readonly record struct TypeScope(string Keyword, string Name, string Constraints);
public sealed record Extensions(Scope Scope, string FullName)
{
public readonly StringBuilder Contents = new();
public readonly List<string> BaseTypes = [];
public readonly List<string> ExtraAttrs = [];
public override string ToString()
{
var sb = new StringBuilder();
// Join all namespaces into a single namespace statement, starting with the outermost.
if (Scope.namespaces.Length > 0)
{
sb.Append("namespace ")
.AppendJoin(".", Scope.namespaces.Reverse())
.AppendLine(" {");
}
// Loop through the full parent type hierarchy, starting with the outermost.
foreach (
var (i, typeScope) in Scope.typeScopes.Select((ts, i) => (i, ts)).Reverse()
)
{
if (i == 0)
{
foreach (var extraAttr in ExtraAttrs)
{
sb.AppendLine(extraAttr);
}
}
sb.Append("partial ")
.Append(typeScope.Keyword) // e.g. class/struct/record
.Append(' ')
.Append(typeScope.Name) // e.g. Outer/Generic<T>
.Append(' ');
if (i == 0 && BaseTypes.Count > 0)
{
sb.Append(" : ").AppendJoin(", ", BaseTypes);
}
if (typeScope.Constraints.Length > 0)
{
sb.Append(' ').Append(typeScope.Constraints);
}
sb.AppendLine(" {");
}
sb.AppendLine();
sb.Append(Contents);
sb.AppendLine();
// We need to "close" each of the parent types, so write
// the required number of '}'
foreach (var typeScope in Scope.typeScopes)
{
sb.Append("} // ").AppendLine(typeScope.Name);
}
// Close the namespace, if we had one
if (Scope.namespaces.Length > 0)
{
sb.AppendLine("} // namespace");
}
return sb.ToString();
}
}
}
}