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