using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
namespace Vellum.Generator
{
///
/// Describes a type built from a method's type parameters, as plain data an incremental generator step can compare
/// between runs.
///
public sealed class SaveTemplate : IEquatable
{
///
/// Holds how deeply a type can nest before it is left out as unsupported, which is as deep as the runtime
/// checks a type for being saveable.
///
public const int MaxDepth = SaveNode.MaxDepth;
///
/// Holds the parameter number of the template for a type that uses a type parameter but can't be spelled.
///
public const int UnspellableParameter = -2;
///
/// Holds the template for a type that uses a type parameter but can't be spelled in an attribute, such as a
/// private type, a pointer, or a type nested too deep.
///
public static readonly SaveTemplate Unspellable = new SaveTemplate(UnspellableParameter, null, default);
///
/// Holds how many digits a type parameter's number can have, which keeps it inside an int.
///
public const int MaxDigits = 9;
///
/// Holds which method type parameter this stands for, or -1 for a type spelled by its format.
///
public readonly int Parameter;
///
/// Holds which type parameter of the holding type this stands for, or -1 if it doesn't stand for one.
///
public readonly int ClassParameter;
///
/// Holds the type's source, with {0}, {1} and so on where each of its parts goes.
///
public readonly string Format;
public readonly EquatableArray Parts;
///
/// Holds whether a type parameter appears anywhere in this.
///
public readonly bool HasParameter;
///
/// Holds whether a type parameter of the holding type appears anywhere in this.
///
public readonly bool HasClassParameter;
///
/// Holds whether appears anywhere in this.
///
public readonly bool HasUnspellable;
///
/// Holds how many types deep this nests, counting itself.
///
public readonly int Depth;
public SaveTemplate(int parameter, string format, EquatableArray parts)
: this(parameter, -1, format, parts)
{
}
public SaveTemplate(int parameter, int classParameter, string format, EquatableArray parts)
{
Parameter = parameter;
ClassParameter = classParameter;
Format = format;
Parts = parts;
Depth = 1;
HasParameter = parameter >= 0 || classParameter >= 0;
HasClassParameter = classParameter >= 0;
HasUnspellable = parameter == UnspellableParameter;
foreach (SaveTemplate part in parts.Items)
{
if (part != null)
{
HasParameter |= part.HasParameter;
HasClassParameter |= part.HasClassParameter;
HasUnspellable |= part.HasUnspellable;
Depth = Math.Max(Depth, part.Depth + 1);
}
}
}
///
/// Returns a template that stands for type parameter number .
///
public static SaveTemplate OfParameter(int parameter)
{
return new SaveTemplate(parameter, null, default);
}
///
/// Returns a template that stands for type parameter number of the type that
/// holds the method.
///
public static SaveTemplate OfClassParameter(int classParameter)
{
return new SaveTemplate(-1, classParameter, null, default);
}
// Returns "global::Outer<{0}, {1}>.Inner<{2}>" for the type, queueing its type arguments in that order.
private static string FormatOf(INamedTypeSymbol type, ImmutableArray.Builder arguments)
{
var chain = new System.Collections.Generic.List();
for (INamedTypeSymbol current = type; current != null; current = current.ContainingType)
{
chain.Add(current);
}
chain.Reverse();
var builder = new StringBuilder("global::");
if (!type.ContainingNamespace.IsGlobalNamespace)
{
builder.Append(type.ContainingNamespace.ToDisplayString()).Append('.');
}
for (int i = 0; i < chain.Count; ++i)
{
if (i > 0)
{
builder.Append('.');
}
builder.Append(chain[i].Name);
if (chain[i].Arity > 0)
{
builder.Append('<');
for (int argument = 0; argument < chain[i].Arity; ++argument)
{
if (argument > 0)
{
builder.Append(", ");
}
builder.Append('{').Append(arguments.Count).Append('}');
arguments.Add(chain[i].TypeArguments[argument]);
}
builder.Append('>');
}
}
return builder.ToString();
}
// An assembly attribute can only name a type every part of the assembly can see.
private static bool CanName(INamedTypeSymbol type)
{
for (INamedTypeSymbol current = type; current != null; current = current.ContainingType)
{
switch (current.DeclaredAccessibility)
{
case Accessibility.Private:
case Accessibility.Protected:
case Accessibility.ProtectedAndInternal:
return false;
}
}
return true;
}
// Returns the digit a single stand-in type names, or -1 for any other type.
private static int DigitOf(ITypeSymbol type)
{
var named = type as INamedTypeSymbol;
if (named == null
|| named.Arity != 0
|| named.ContainingNamespace.ToDisplayString() != "Vellum"
|| !named.Name.StartsWith(SaveMethods.ParameterName, StringComparison.Ordinal)
|| named.Name.Length != SaveMethods.ParameterName.Length + 1)
{
return -1;
}
char digit = named.Name[named.Name.Length - 1];
return digit >= '0' && digit <= '9' ? digit - '0' : -1;
}
///
/// Returns the number a stand-in type names, or -1 for any other type.
///
public static int StandInNumber(ITypeSymbol type)
{
// The last digit comes first, since each generic stand-in wraps the number without its last digit.
var digits = new List();
ITypeSymbol current = type;
while (true)
{
int digit = DigitOf(current);
if (digit >= 0)
{
if (digits.Count >= MaxDigits)
{
return -1;
}
digits.Add(digit);
break;
}
var named = current as INamedTypeSymbol;
if (named == null
|| named.Arity != 2
|| named.Name != SaveMethods.ParameterName
|| named.ContainingNamespace.ToDisplayString() != "Vellum")
{
return -1;
}
int last = DigitOf(named.TypeArguments[1]);
if (last < 0 || digits.Count >= MaxDigits)
{
return -1;
}
digits.Add(last);
current = named.TypeArguments[0];
}
int returnMe = 0;
for (int i = digits.Count - 1; i >= 0; --i)
{
returnMe = returnMe * 10 + digits[i];
}
return returnMe;
}
///
/// Returns the number a class stand-in type names, or -1 for any other type.
///
public static int ClassStandInNumber(ITypeSymbol type)
{
var named = type as INamedTypeSymbol;
if (named == null
|| named.Arity != 1
|| named.Name != SaveMethods.ClassParameterName
|| named.ContainingNamespace.ToDisplayString() != "Vellum")
{
return -1;
}
return StandInNumber(named.TypeArguments[0]);
}
///
/// Returns the number of a type's type parameter among the type parameters of the type and the types around
/// it, which is how a documentation ID numbers them.
///
public static int ClassOrdinalOf(ITypeParameterSymbol parameter)
{
int returnMe = parameter.Ordinal;
for (INamedTypeSymbol outer = parameter.DeclaringType?.ContainingType; outer != null;
outer = outer.ContainingType)
{
returnMe += outer.Arity;
}
return returnMe;
}
private static SaveTemplate Build(ITypeSymbol type, int depth, ref IMethodSymbol owner)
{
if (depth > MaxDepth)
{
return null;
}
var parameter = type as ITypeParameterSymbol;
if (parameter != null)
{
if (parameter.TypeParameterKind == TypeParameterKind.Type)
{
return OfClassParameter(ClassOrdinalOf(parameter));
}
if (parameter.TypeParameterKind != TypeParameterKind.Method
|| (owner != null && !SymbolEqualityComparer.Default.Equals(owner, parameter.DeclaringMethod)))
{
return null;
}
owner = parameter.DeclaringMethod;
return OfParameter(parameter.Ordinal);
}
ImmutableArray.Builder children = ImmutableArray.CreateBuilder();
string format;
var array = type as IArrayTypeSymbol;
if (array != null)
{
// The outermost array's rank comes first in C#, so int[][,] is an array of two-dimensional arrays.
var ranks = new StringBuilder("{0}");
ITypeSymbol element = array;
while (element is IArrayTypeSymbol)
{
var current = (IArrayTypeSymbol)element;
ranks.Append('[').Append(',', current.Rank - 1).Append(']');
element = current.ElementType;
}
children.Add(element);
format = ranks.ToString();
}
else
{
var named = type as INamedTypeSymbol;
if (named == null || named.TypeKind == TypeKind.Error || named.IsAnonymousType || !CanName(named))
{
return null;
}
int standIn = StandInNumber(named);
if (standIn >= 0)
{
return OfParameter(standIn);
}
int classStandIn = ClassStandInNumber(named);
if (classStandIn >= 0)
{
return OfClassParameter(classStandIn);
}
format = FormatOf(named, children);
}
ImmutableArray.Builder parts = ImmutableArray.CreateBuilder();
foreach (ITypeSymbol child in children)
{
SaveTemplate part = Build(child, depth + 1, ref owner);
if (part == null)
{
return null;
}
parts.Add(part);
}
return new SaveTemplate(-1, format, new EquatableArray(parts.ToImmutable()));
}
///
/// Returns as a template, or null if it uses something a registration can't spell.
///
///
/// The type to describe. A method's own type parameter becomes a stand-in for it, and so does a type
/// parameter of the type around the method and one of Vellum's stand-in types.
///
///
/// The method whose type parameters uses. A type that uses another method's is null.
///
public static SaveTemplate Of(ITypeSymbol type, ref IMethodSymbol owner)
{
return Build(type, 1, ref owner);
}
///
/// Returns this with each type parameter replaced by the argument of the same number, or null if one has no
/// argument to take.
///
///
/// The result can be deeper than , since an argument can nest as deep itself.
///
/// What the method's own type parameters stand for.
/// What the type parameters of the type holding the method stand for.
public SaveTemplate Fill(ImmutableArray arguments, ImmutableArray classArguments)
{
if (Parameter >= 0)
{
return Parameter < arguments.Length ? arguments[Parameter] : null;
}
if (ClassParameter >= 0)
{
return ClassParameter < classArguments.Length ? classArguments[ClassParameter] : null;
}
if (!HasParameter)
{
return this;
}
ImmutableArray parts = Parts.Items;
ImmutableArray.Builder filled = ImmutableArray.CreateBuilder(parts.Length);
foreach (SaveTemplate part in parts)
{
SaveTemplate filledPart = part?.Fill(arguments, classArguments);
if (filledPart == null)
{
return null;
}
filled.Add(filledPart);
}
return new SaveTemplate(-1, Format, new EquatableArray(filled.ToImmutable()));
}
///
/// Returns this with each type parameter of the holding type renumbered, or null if one has no number to
/// take.
///
/// Holds the new number of each old one, or a negative number for none.
public SaveTemplate RemapClassParameters(ImmutableArray map)
{
if (ClassParameter >= 0)
{
return ClassParameter < map.Length && map[ClassParameter] >= 0
? OfClassParameter(map[ClassParameter])
: null;
}
if (!HasClassParameter)
{
return this;
}
ImmutableArray parts = Parts.Items;
ImmutableArray.Builder remapped = ImmutableArray.CreateBuilder(parts.Length);
foreach (SaveTemplate part in parts)
{
SaveTemplate remappedPart = part?.RemapClassParameters(map);
if (remappedPart == null)
{
return null;
}
remapped.Add(remappedPart);
}
return new SaveTemplate(-1, Format, new EquatableArray(remapped.ToImmutable()));
}
///
/// Returns this with each method type parameter replaced, for a method of a type that has no type parameters.
///
public SaveTemplate Fill(ImmutableArray arguments)
{
return Fill(arguments, ImmutableArray.Empty);
}
// Writes a number as one stand-in, or as the stand-in for its leading digits wrapped around its last digit.
private static string ParameterSource(int number)
{
string digits = number.ToString(System.Globalization.CultureInfo.InvariantCulture);
string returnMe = "global::Vellum." + SaveMethods.ParameterName + digits[0];
for (int i = 1; i < digits.Length; ++i)
{
returnMe = "global::Vellum." + SaveMethods.ParameterName + "<" + returnMe + ", global::Vellum." +
SaveMethods.ParameterName + digits[i] + ">";
}
return returnMe;
}
///
/// Returns the C# source of this type, with each type parameter spelled as its stand-in type.
///
/// This holds a type that can't be spelled.
public string ToSource()
{
if (HasUnspellable)
{
throw new InvalidOperationException("A type that can't be spelled in an attribute has no source.");
}
if (Parameter >= 0)
{
return ParameterSource(Parameter);
}
if (ClassParameter >= 0)
{
return "global::Vellum." + SaveMethods.ClassParameterName + "<" + ParameterSource(ClassParameter) + ">";
}
ImmutableArray parts = Parts.Items;
var sources = new object[parts.Length];
for (int i = 0; i < sources.Length; ++i)
{
sources[i] = parts[i].ToSource();
}
return string.Format(Format, sources);
}
public bool Equals(SaveTemplate other)
{
return other != null
&& Parameter == other.Parameter
&& ClassParameter == other.ClassParameter
&& Format == other.Format
&& Parts.Equals(other.Parts);
}
public override bool Equals(object obj)
{
return Equals(obj as SaveTemplate);
}
public override int GetHashCode()
{
int returnMe = Parameter * 31 + ClassParameter;
return ((returnMe * 31 + (Format == null ? 0 : Format.GetHashCode())) * 31) + Parts.GetHashCode();
}
}
}