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); } } }