using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace Vellum.Generator
{
///
/// Describes everything the writer needs to generate a [Saveable] type's half, as plain data. A symbol can't be
/// compared between runs of an incremental generator, since each edit makes new ones.
///
public sealed class SaveableShape : IEquatable
{
public const string FileSuffix = ".Saveable.g.cs";
///
/// Holds the name of the generated file.
///
public readonly string FileName;
///
/// Holds the namespace the type is in, or an empty string for the global namespace.
///
public readonly string NamespaceName;
///
/// Holds how each type around the type is redeclared, outermost first and the type itself last.
///
public readonly EquatableArray Declarations;
public readonly string Name;
///
/// Holds the type, spelled so generated code names it from anywhere.
///
public readonly string TypeName;
public readonly string ConverterFullName;
public readonly EquatableArray Members;
public readonly bool WritesOwnLoader;
public readonly bool ChainsToParameterless;
public readonly bool KeepsImplicitConstructor;
///
/// Holds whether the type is a class others can derive from, so its loaders must be protected.
///
public readonly bool IsDerivable;
///
/// Holds whether the type derives from another [Saveable] class.
///
public readonly bool HasSaveableBase;
///
/// Holds whether the type is abstract, so it gets no converter.
///
public readonly bool IsAbstract;
///
/// Holds whether a [Saveable] class above this one gets a converter, which this type's converter then hides.
///
public readonly bool HidesBaseConverter;
///
/// Holds whether the type is a class, which can be handed out part-built to a value that refers back to it.
///
public readonly bool IsClass;
///
/// Holds the base class, spelled so generated code names it from anywhere, or an empty string when the type
/// has no [Saveable] base.
///
public readonly string BaseTypeName;
///
/// Holds the members that aren't of a built-in type, which are the ones loading has to descend into.
///
public readonly EquatableArray ChildMembers;
public SaveableShape(string fileName, string namespaceName, EquatableArray declarations, string name,
string typeName, string converterFullName, EquatableArray members, bool writesOwnLoader,
bool chainsToParameterless, bool keepsImplicitConstructor, bool isDerivable, bool hasSaveableBase,
bool isAbstract, bool hidesBaseConverter, bool isClass, string baseTypeName,
EquatableArray childMembers)
{
FileName = fileName;
NamespaceName = namespaceName;
Declarations = declarations;
Name = name;
TypeName = typeName;
ConverterFullName = converterFullName;
Members = members;
WritesOwnLoader = writesOwnLoader;
ChainsToParameterless = chainsToParameterless;
KeepsImplicitConstructor = keepsImplicitConstructor;
IsDerivable = isDerivable;
HasSaveableBase = hasSaveableBase;
IsAbstract = isAbstract;
HidesBaseConverter = hidesBaseConverter;
IsClass = isClass;
BaseTypeName = baseTypeName;
ChildMembers = childMembers;
}
private static string FullName(ITypeSymbol type)
{
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
// A saved member is a field or an auto-property. Either can be written by code in the type, unless it's
// readonly or its property has no setter beyond an init one, which only a constructor may call.
private static bool AssignsDirectly(ISymbol declared)
{
var field = declared as IFieldSymbol;
if (field != null)
{
return !field.IsReadOnly;
}
var property = declared as IPropertySymbol;
return property != null && property.SetMethod != null && !property.SetMethod.IsInitOnly;
}
// Repeats the type's own static and readonly, so the redeclaration matches it.
private static string Declaration(INamedTypeSymbol type)
{
string returnMe = "partial class ";
if (type.TypeKind == TypeKind.Struct)
{
returnMe = "partial struct ";
}
if (type.IsReadOnly)
{
returnMe = "readonly " + returnMe;
}
if (type.IsStatic)
{
returnMe = "static " + returnMe;
}
// Constraints are left out, since a partial type's other part declares them.
returnMe += type.Name;
if (type.Arity > 0)
{
returnMe += "<" + string.Join(", ", type.TypeParameters.Select(parameter => parameter.Name)) + ">";
}
return returnMe;
}
// A hint name can't hold the angle brackets of a generic type, so each generic type gets its arity instead.
private static string FileNameOf(INamedTypeSymbol type)
{
var names = new List();
for (INamedTypeSymbol current = type; current != null; current = current.ContainingType)
{
string name = current.Name;
if (current.Arity > 0)
{
name += "`" + current.Arity;
}
names.Insert(0, name);
}
string returnMe = string.Join(".", names);
if (!type.ContainingNamespace.IsGlobalNamespace)
{
returnMe = type.ContainingNamespace.ToDisplayString() + "." + returnMe;
}
return returnMe + FileSuffix;
}
// A converter nested in a generic type is generic too, so it's registered open.
private static string ConverterNameOf(INamedTypeSymbol type)
{
INamedTypeSymbol open = type;
if (type.IsGenericType)
{
open = type.ConstructUnboundGenericType();
}
return FullName(open) + "." + SaveableClass.ConverterName;
}
///
/// Returns the shape of , which saves .
///
/// Whether the type writes its own SaveReader constructor.
/// Whether loading starts from the parameterless constructor.
///
/// Whether the type is a class whose only constructor is the one C# writes for it.
///
public static SaveableShape Of(INamedTypeSymbol type, List saved, bool writesOwnLoader,
bool chainsToParameterless, bool keepsImplicitConstructor)
{
var declarations = new List();
for (INamedTypeSymbol current = type; current != null; current = current.ContainingType)
{
declarations.Insert(0, Declaration(current));
}
var members = new List();
var childMembers = new List();
foreach (SaveableMember member in saved)
{
var shape = new MemberShape(member.Key, FullName(member.Type), AssignsDirectly(member.Declared));
members.Add(shape);
if (!SaveableTypes.IsBuiltIn(member.Type))
{
childMembers.Add(shape);
}
}
string namespaceName = "";
if (!type.ContainingNamespace.IsGlobalNamespace)
{
namespaceName = type.ContainingNamespace.ToDisplayString();
}
bool isClass = type.TypeKind == TypeKind.Class;
bool hasSaveableBase = isClass && type.BaseType.SpecialType != SpecialType.System_Object;
bool hidesBaseConverter = false;
string baseTypeName = "";
if (hasSaveableBase)
{
baseTypeName = FullName(type.BaseType);
for (INamedTypeSymbol current = type.BaseType; current != null && SaveableClass.IsMarked(current);
current = current.BaseType)
{
hidesBaseConverter |= !current.IsAbstract;
}
}
return new SaveableShape(FileNameOf(type),
namespaceName,
new EquatableArray(declarations.ToImmutableArray()),
type.Name,
FullName(type),
ConverterNameOf(type),
new EquatableArray(members.ToImmutableArray()),
writesOwnLoader,
chainsToParameterless,
keepsImplicitConstructor,
isDerivable: isClass && !type.IsSealed,
hasSaveableBase: hasSaveableBase,
isAbstract: type.IsAbstract,
hidesBaseConverter: hidesBaseConverter,
isClass: isClass,
baseTypeName: baseTypeName,
childMembers: new EquatableArray(childMembers.ToImmutableArray())
);
}
public bool Equals(SaveableShape other)
{
return other != null
&& FileName == other.FileName
&& NamespaceName == other.NamespaceName
&& Declarations.Equals(other.Declarations)
&& Name == other.Name
&& TypeName == other.TypeName
&& ConverterFullName == other.ConverterFullName
&& Members.Equals(other.Members)
&& WritesOwnLoader == other.WritesOwnLoader
&& ChainsToParameterless == other.ChainsToParameterless
&& KeepsImplicitConstructor == other.KeepsImplicitConstructor
&& IsDerivable == other.IsDerivable
&& HasSaveableBase == other.HasSaveableBase
&& IsAbstract == other.IsAbstract
&& HidesBaseConverter == other.HidesBaseConverter
&& IsClass == other.IsClass
&& BaseTypeName == other.BaseTypeName
&& ChildMembers.Equals(other.ChildMembers);
}
public override bool Equals(object obj)
{
return Equals(obj as SaveableShape);
}
public override int GetHashCode()
{
return (FileName.GetHashCode() * 31 + Declarations.GetHashCode()) * 31 + Members.GetHashCode();
}
}
}