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