using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Vellum.Generator
{
///
/// Reads a [Saveable] type, in whatever shape it is, into the plain data the generator works from.
///
public static class SaveableClass
{
public const string AttributeName = "Vellum.SaveableAttribute";
public const string NoSaveAttributeName = "Vellum.NoSaveAttribute";
public const string ReaderName = "Vellum.SaveReader";
public const string ConverterName = "__Converter";
private static bool HasAttribute(ISymbol symbol, string name)
{
return symbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == name);
}
///
/// Returns true if itself carries [Saveable].
///
public static bool HasOwnMark(INamedTypeSymbol type)
{
return HasAttribute(type, AttributeName);
}
///
/// Returns true if a class derives from, or an interface it implements or extends,
/// carries [Saveable].
///
public static bool InheritsMark(INamedTypeSymbol type)
{
for (INamedTypeSymbol current = type.BaseType; current != null; current = current.BaseType)
{
if (HasOwnMark(current))
{
return true;
}
}
// AllInterfaces includes the ones a base class implements and the ones an interface extends.
foreach (INamedTypeSymbol implemented in type.AllInterfaces)
{
if (HasOwnMark(implemented))
{
return true;
}
}
return false;
}
///
/// Returns true if is [Saveable], whether it says so or inherits it.
///
public static bool IsMarked(INamedTypeSymbol type)
{
return HasOwnMark(type) || InheritsMark(type);
}
///
/// Returns true if is the first part of to list what it
/// derives from.
///
public static bool IsFirstBaseList(INamedTypeSymbol type, SyntaxNode declaration)
{
foreach (SyntaxReference reference in type.DeclaringSyntaxReferences)
{
var part = reference.GetSyntax() as TypeDeclarationSyntax;
if (part != null && part.BaseList != null)
{
return part == declaration;
}
}
return false;
}
///
/// Returns true if holds the first [Saveable] has.
///
public static bool IsFirstMark(INamedTypeSymbol type, SyntaxNode declaration)
{
AttributeData first = type.GetAttributes().First(
attribute => attribute.AttributeClass?.ToDisplayString() == AttributeName);
SyntaxReference applied = first.ApplicationSyntaxReference;
return applied != null
&& applied.SyntaxTree == declaration.SyntaxTree
&& declaration.Span.Contains(applied.Span);
}
private static int DeclarationOrder(SaveableMember member)
{
return member.Declared.Locations.First().SourceSpan.Start;
}
private static string DeclarationFile(SaveableMember member)
{
return member.Declared.Locations.First().SourceTree.FilePath;
}
///
/// Returns the members saves: every instance field and auto-property not marked
/// [NoSave]. Works on a type from a referenced assembly, whose members have no source to put them in order by.
///
public static List SavedMembersOf(INamedTypeSymbol type)
{
var returnMe = new List();
foreach (IFieldSymbol field in type.GetMembers().OfType())
{
ISymbol declared = field.AssociatedSymbol ?? field;
if (field.IsStatic
|| field.IsConst
|| HasAttribute(field, NoSaveAttributeName)
|| HasAttribute(declared, NoSaveAttributeName))
{
continue;
}
// Only an auto-property's backing field is named for something the type declared.
if (field.IsImplicitlyDeclared && !(declared is IPropertySymbol))
{
continue;
}
returnMe.Add(new SaveableMember(declared.Name, field.Type, declared));
}
return returnMe;
}
///
/// Returns the members saves, in declaration order.
///
public static List MembersOf(INamedTypeSymbol type)
{
return SavedMembersOf(type)
.OrderBy(DeclarationFile, System.StringComparer.Ordinal)
.ThenBy(DeclarationOrder)
.ToList();
}
private static bool IsPartial(INamedTypeSymbol type)
{
return type.DeclaringSyntaxReferences
.Select(reference => reference.GetSyntax())
.OfType()
.All(declaration => declaration.Modifiers.Any(SyntaxKind.PartialKeyword));
}
// Generated code redeclares the type and each type around it, which takes partial classes or structs.
private static bool CanRedeclare(INamedTypeSymbol type)
{
return IsPartial(type)
&& (type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct)
&& !type.IsRecord;
}
// A base that isn't [Saveable] would have its fields silently left out of every save.
private static bool HasSupportedShape(INamedTypeSymbol type)
{
for (INamedTypeSymbol current = type; current != null; current = current.ContainingType)
{
if (!CanRedeclare(current))
{
return false;
}
}
return SymbolSearch.IsPublic(type)
&& !type.IsStatic
&& (type.TypeKind == TypeKind.Struct
|| type.BaseType.SpecialType == SpecialType.System_Object
|| IsMarked(type.BaseType));
}
// The constructor a class others derive from is called by them, so it can't be private.
private static Accessibility LoaderAccess(INamedTypeSymbol type)
{
if (type.TypeKind == TypeKind.Class && !type.IsSealed)
{
return Accessibility.Protected;
}
return Accessibility.Private;
}
///
/// Returns the members saves, followed by those of each [Saveable] class it derives
/// from, with their types as sees them.
///
public static List SavedMembersWithBasesOf(INamedTypeSymbol type)
{
List returnMe = SavedMembersOf(type);
for (INamedTypeSymbol current = type.BaseType; current != null && IsMarked(current);
current = current.BaseType)
{
returnMe.AddRange(SavedMembersOf(current));
}
return returnMe;
}
// Each saved key of each [Saveable] class this one derives from, so a member can't be saved over theirs.
private static Dictionary KeysSavedByBases(INamedTypeSymbol type)
{
var returnMe = new Dictionary(System.StringComparer.Ordinal);
for (INamedTypeSymbol current = type.BaseType; current != null && IsMarked(current);
current = current.BaseType)
{
foreach (SaveableMember member in SavedMembersOf(current))
{
if (!returnMe.ContainsKey(member.Key))
{
returnMe.Add(member.Key, current.Name);
}
}
}
return returnMe;
}
private static IMethodSymbol OwnLoader(INamedTypeSymbol type)
{
return type.InstanceConstructors.FirstOrDefault(constructor => constructor.Parameters.Length == 1
&& constructor.Parameters[0].Type.ToDisplayString() == ReaderName);
}
private static ImmutableArray MemberChecks(INamedTypeSymbol type, List members)
{
Dictionary savedByBases = KeysSavedByBases(type);
ImmutableArray.Builder returnMe = ImmutableArray.CreateBuilder();
foreach (SaveableMember member in members)
{
string hiddenIn;
savedByBases.TryGetValue(member.Key, out hiddenIn);
returnMe.Add(new MemberCheck(member.Key, TypeShape.Of(member.Type),
SourcePlace.Of(member.Declared.Locations.FirstOrDefault()), hiddenIn));
}
// A generic base is only checked where it's given its arguments, so its members are checked here.
SourcePlace derivedPlace = SourcePlace.Of(type.Locations.FirstOrDefault());
for (INamedTypeSymbol current = type.BaseType; current != null && IsMarked(current);
current = current.BaseType)
{
if (!current.IsGenericType)
{
continue;
}
foreach (SaveableMember member in SavedMembersOf(current))
{
returnMe.Add(new MemberCheck(member.Key, TypeShape.Of(member.Type), derivedPlace, hiddenIn: null));
}
}
return returnMe.ToImmutable();
}
///
/// Returns as plain data, whether or not it's a shape a converter can be written for.
///
public static SaveableModel Describe(INamedTypeSymbol type)
{
// An interface has nothing to save or construct, so only the types that implement it get code.
if (type.TypeKind == TypeKind.Interface)
{
return new SaveableModel(shape: null, new SaveableChecks(type.Name, SaveableTypes.NameOf(type),
generates: false, SaveableProblem.None, SourcePlace.Of(null), inheritsMark: false,
new EquatableArray()));
}
List members = MembersOf(type);
IMethodSymbol loader = OwnLoader(type);
// A public back door would sit in IntelliSense for anyone to construct a half-loaded instance with.
SaveableProblem problem = SaveableProblem.None;
Location problemLocation = null;
if (!HasSupportedShape(type))
{
problem = SaveableProblem.Unsupported;
problemLocation = type.Locations.First();
}
else if (loader != null && loader.DeclaredAccessibility != LoaderAccess(type))
{
problem = SaveableProblem.PublicLoader;
problemLocation = loader.Locations.First();
}
// A type that already has a converter nested in it keeps that one.
SaveableShape shape = null;
if (problem == SaveableProblem.None && type.GetTypeMembers(ConverterName).Length == 0)
{
bool chainsToParameterless = type.TypeKind == TypeKind.Struct
|| type.InstanceConstructors.Any(constructor => constructor.Parameters.Length == 0);
bool keepsImplicitConstructor = type.TypeKind == TypeKind.Class
&& type.InstanceConstructors.All(constructor => constructor.IsImplicitlyDeclared);
shape = SaveableShape.Of(type, members, writesOwnLoader: loader != null,
chainsToParameterless: chainsToParameterless, keepsImplicitConstructor: keepsImplicitConstructor);
}
var checks = new SaveableChecks(type.Name, SaveableTypes.NameOf(type),
generates: shape != null && !type.IsAbstract, problem, SourcePlace.Of(problemLocation),
inheritsMark: !HasOwnMark(type), new EquatableArray(MemberChecks(type, members)));
return new SaveableModel(shape, checks);
}
}
}