using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; namespace Vellum.Generator { /// /// Describes a type something saves and the types it's built from, as plain data, and decides whether all of it can /// be saved. A symbol can't be compared between runs of an incremental generator, since each edit makes new ones. /// public sealed class TypeShape : IEquatable { /// /// Holds how deeply a type's parts may nest, which is how deeply the runtime checks a type for being saveable. /// public const int MaxDepth = SaveNode.MaxDepth; /// /// Holds how many parts a shape may have before a generic [Saveable] type stops being looked into. /// public const int MaxParts = 1024; /// /// Holds how C# spells the whole type, as the code that saves it wrote it. /// public readonly string Display; /// /// Holds the type first, then its parts. Each part's own parts sit together after the parts before it. /// public readonly EquatableArray Parts; public TypeShape(string display, EquatableArray parts) { Display = display; Parts = parts; } private static PartKind KindOf(ITypeSymbol type) { if (type.TypeKind == TypeKind.TypeParameter || type.TypeKind == TypeKind.Error) { return PartKind.Ignored; } var named = type as INamedTypeSymbol; if (named != null && named.IsGenericType && SaveableClass.IsMarked(named)) { return PartKind.SavedMembers; } if (SaveableTypes.IsBuiltIn(type) || (named != null && SaveableClass.IsMarked(named))) { return PartKind.Whole; } if (type is IArrayTypeSymbol) { return PartKind.Collection; } if (named == null || !named.IsGenericType) { return PartKind.Plain; } if (SaveableTypes.IsBuiltInCollection(named)) { return PartKind.Collection; } return PartKind.Generic; } // The types a collection or generic type is built from, or the types of a generic [Saveable] type's members. private static List PartsOf(ITypeSymbol type, PartKind kind) { var array = type as IArrayTypeSymbol; if (array != null) { return new List { array.ElementType }; } var named = (INamedTypeSymbol)type; if (kind == PartKind.SavedMembers) { return SaveableClass.SavedMembersWithBasesOf(named).Select(member => member.Type).ToList(); } return SaveableTypes.ArgumentsOf(named); } // Queues the type arguments of each generic class a family could match. They are looked at only when a family // marks a type parameter [Saveable]. The type's own arguments are already queued as its parts. private static EquatableArray AddBaseArguments(ITypeSymbol type, bool ownArgumentsQueued, int firstChild, int childCount, List symbols, int depth, List depths) { ImmutableArray.Builder returnMe = ImmutableArray.CreateBuilder(); if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct && type.TypeKind != TypeKind.Interface) { return default; } var linked = new List(); for (ITypeSymbol link = type; link != null && link.SpecialType != SpecialType.System_Object; link = link.BaseType) { var named = link as INamedTypeSymbol; if (named != null && named.IsGenericType) { linked.Add(named); } } linked.AddRange(type.AllInterfaces.Where(implemented => implemented.IsGenericType)); foreach (INamedTypeSymbol named in linked) { string name = SaveableTypes.NameOf(named.OriginalDefinition); if (ReferenceEquals(named, type) && ownArgumentsQueued) { returnMe.Add(new BaseArguments(name, firstChild, childCount)); continue; } List arguments = SaveableTypes.ArgumentsOf(named); returnMe.Add(new BaseArguments(name, symbols.Count, arguments.Count)); foreach (ITypeSymbol argument in arguments) { symbols.Add(argument); depths.Add(depth); } } return new EquatableArray(returnMe.ToImmutable()); } /// /// Returns the shape of . /// public static TypeShape Of(ITypeSymbol type) { // Parts go in a queue's order, so the parts of one type end up together without any recursion. var symbols = new List { type }; var depths = new List { 1 }; ImmutableArray.Builder parts = ImmutableArray.CreateBuilder(); var expanded = new HashSet(); var queuedBases = new Dictionary>(); for (int i = 0; i < symbols.Count; ++i) { ITypeSymbol current = SaveableTypes.WithoutNullable(symbols[i]); PartKind kind = KindOf(current); // A type that holds itself is checked at its first appearance, which is what keeps it from looping. // One that keeps growing as it nests is cut off by the part limit, and counts as unsaveable. if (kind == PartKind.SavedMembers && !expanded.Add(SaveableTypes.NameOf(current))) { kind = PartKind.Whole; } else if (kind == PartKind.SavedMembers && symbols.Count > MaxParts) { kind = PartKind.Plain; } string openName = null; var named = current as INamedTypeSymbol; if (named != null && named.IsGenericType) { openName = SaveableTypes.NameOf(named.OriginalDefinition); } // Nothing past the depth limit is looked at, which also keeps a deeply nested type from being walked. int firstChild = symbols.Count; int childCount = 0; bool hasParts = kind == PartKind.Generic || kind == PartKind.Collection || kind == PartKind.SavedMembers; if (hasParts && depths[i] <= MaxDepth) { List children = PartsOf(current, kind); childCount = children.Count; foreach (ITypeSymbol child in children) { symbols.Add(child); depths.Add(depths[i] + 1); } } EquatableArray familyNames = default; EquatableArray baseArguments = default; if (kind == PartKind.Plain || kind == PartKind.Generic) { familyNames = SaveableTypes.FamilyNamesOf(current); // A type met again shares the arguments queued the first time, so a type that implements an // interface made from itself doesn't keep queueing. string typeName = SaveableTypes.NameOf(current); if (!queuedBases.TryGetValue(typeName, out baseArguments) && depths[i] <= MaxDepth && symbols.Count <= MaxParts) { baseArguments = AddBaseArguments(current, kind == PartKind.Generic, firstChild, childCount, symbols, depths[i] + 1, depths); queuedBases.Add(typeName, baseArguments); } } parts.Add(new TypePart(SaveableTypes.NameOf(current), openName, kind, firstChild, childCount, familyNames, baseArguments)); } return new TypeShape(type.ToDisplayString(), new EquatableArray(parts.ToImmutable())); } // Queues the arguments of the base class a family saves that the family requires to be saveable. A type met // again shares the arguments of its first appearance, so each is queued once or a type made from itself would // loop. private void PushFamilyArguments(TypePart part, ImmutableArray positions, string familyBase, int depth, Stack> pending, HashSet queued) { if (positions.Length == 0) { return; } BaseArguments arguments = part.BaseArguments.Items.FirstOrDefault( candidate => candidate.Name == familyBase); if (arguments == null) { return; } for (int i = positions.Length - 1; i >= 0; --i) { if (positions[i] < arguments.ChildCount && queued.Add(arguments.FirstChild + positions[i])) { pending.Push(new KeyValuePair(arguments.FirstChild + positions[i], depth + 1)); } } } /// /// Returns the index of the innermost part that can't be saved, which is 0, the type itself, if nothing /// smaller is to blame, or -1 if all of it can be saved. holds the name of each /// type something has a converter for, and of each generic type something has a generic converter for. /// holds, for each base class or interface something has a family converter for, /// which of its type arguments the converter marks [Saveable]. A type derived from one is saved whole, except /// that those arguments must be saveable too. holds, for each generic /// type with a generic converter, which of its type arguments must be saveable. The others can be anything, /// and a generic type missing from it needs every argument saveable. /// public int FindUnsaveable(HashSet converted, Dictionary> families, Dictionary> saveableArguments) { var pending = new Stack>(); var familyQueued = new HashSet(); pending.Push(new KeyValuePair(0, 1)); while (pending.Count > 0) { KeyValuePair entry = pending.Pop(); TypePart part = Parts.Items[entry.Key]; if (part.Kind == PartKind.Ignored || part.Kind == PartKind.Whole || converted.Contains(part.Name)) { continue; } string familyBase = part.FamilyNames.Items.FirstOrDefault(families.ContainsKey); if (familyBase != null) { PushFamilyArguments(part, families[familyBase].Items, familyBase, entry.Value, pending, familyQueued); continue; } bool savedFromParts = part.Kind == PartKind.Collection || part.Kind == PartKind.SavedMembers || (part.Kind == PartKind.Generic && converted.Contains(part.OpenName)); if (!savedFromParts || entry.Value > MaxDepth) { return entry.Key; } EquatableArray required; if (part.Kind == PartKind.Generic && saveableArguments.TryGetValue(part.OpenName, out required)) { ImmutableArray positions = required.Items; for (int i = positions.Length - 1; i >= 0; --i) { if (positions[i] < part.ChildCount) { pending.Push(new KeyValuePair(part.FirstChild + positions[i], entry.Value + 1)); } } continue; } for (int i = part.ChildCount - 1; i >= 0; --i) { pending.Push(new KeyValuePair(part.FirstChild + i, entry.Value + 1)); } } return -1; } /// /// Returns the report that something saved as can't be saved because of the part /// at , the index found. /// public Diagnostic UnsaveableDiagnostic(Location location, string savedName, int unsaveable) { string context = ""; if (unsaveable != 0) { context = " (inside " + Display + ")"; } return Diagnostic.Create(VellumDiagnostics.UnsaveableType, location, savedName, Parts.Items[unsaveable].Name, context); } public bool Equals(TypeShape other) { return other != null && Display == other.Display && Parts.Equals(other.Parts); } public override bool Equals(object obj) { return Equals(obj as TypeShape); } public override int GetHashCode() { return Display.GetHashCode() * 31 + Parts.GetHashCode(); } } }