using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Vellum.Generator { /// /// Finds each SaveConverter a compilation can save with, as data an incremental generator can compare between runs. /// public static class ConverterScan { public const string ConverterMetadataName = "Vellum.SaveConverter`1"; private const string ConverterBaseName = "SaveConverter`1"; /// /// Returns the type converts, or null if it isn't a concrete converter. A generic /// converter's type is returned still open, built from the converter's own type parameters. /// public static ITypeSymbol ConvertedBy(INamedTypeSymbol type, INamedTypeSymbol converterBase) { if (type.IsAbstract) { return null; } return ConvertedByAnyKind(type, converterBase); } /// /// Returns the type converts, or null if it doesn't derive from the converter base. /// An abstract converter counts, since it's the converters derived from it that are used. /// public static ITypeSymbol ConvertedByAnyKind(INamedTypeSymbol type, INamedTypeSymbol converterBase) { for (INamedTypeSymbol baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) { if (SymbolEqualityComparer.Default.Equals(baseType.OriginalDefinition, converterBase)) { return baseType.TypeArguments[0]; } } return null; } // Includes the type parameters of any type the converter is nested in, which it may use as well. private static List ParametersOf(INamedTypeSymbol converter) { var returnMe = new List(); for (INamedTypeSymbol type = converter; type != null; type = type.ContainingType) { returnMe.InsertRange(0, type.TypeParameters); } return returnMe; } /// /// Returns the base class or interface that a generic of /// saves every derived or implementing type of, or null if /// isn't a type parameter that does that. The type parameter must have a class or /// interface for its only constraint, and that type's arguments must be exactly the converter's other type /// parameters, each used once. /// public static INamedTypeSymbol FamilyBaseOf(INamedTypeSymbol converter, ITypeSymbol converted) { var subject = converted as ITypeParameterSymbol; if (subject == null || !SymbolEqualityComparer.Default.Equals(subject.ContainingSymbol, converter) || subject.HasReferenceTypeConstraint || subject.HasValueTypeConstraint || subject.HasConstructorConstraint || subject.HasUnmanagedTypeConstraint || subject.HasNotNullConstraint || subject.ConstraintTypes.Length != 1) { return null; } var familyBase = subject.ConstraintTypes[0] as INamedTypeSymbol; if (familyBase == null || (familyBase.TypeKind != TypeKind.Class && familyBase.TypeKind != TypeKind.Interface)) { return null; } List parameters = ParametersOf(converter); parameters.RemoveAll(parameter => SymbolEqualityComparer.Default.Equals(parameter, subject)); List arguments = SaveableTypes.ArgumentsOf(familyBase); if (arguments.Count != parameters.Count) { return null; } var used = new HashSet(SymbolEqualityComparer.Default); foreach (ITypeSymbol argument in arguments) { if (!parameters.Any(parameter => SymbolEqualityComparer.Default.Equals(parameter, argument)) || !used.Add(argument)) { return null; } } return familyBase; } /// /// Returns true if , what a generic converts, is a /// generic type built from exactly the converter's type parameters, each used once, or a type parameter that /// stands for any type derived from a base class. /// public static bool MatchesShape(INamedTypeSymbol converter, ITypeSymbol converted) { if (converted is ITypeParameterSymbol) { return FamilyBaseOf(converter, converted) != null; } List parameters = ParametersOf(converter); var named = converted as INamedTypeSymbol; if (named == null || !named.IsGenericType) { return false; } List arguments = SaveableTypes.ArgumentsOf(named); if (arguments.Count != parameters.Count) { return false; } var used = new HashSet(SymbolEqualityComparer.Default); foreach (ITypeSymbol argument in arguments) { if (!parameters.Any(parameter => SymbolEqualityComparer.Default.Equals(parameter, argument)) || !used.Add(argument)) { return false; } } return true; } // Every argument of the type must be one of the parameters, and every parameter an argument, each used once. private static string ArgumentMismatchOf(List parameters, INamedTypeSymbol made, string parameterKind) { var used = new HashSet(SymbolEqualityComparer.Default); List arguments = SaveableTypes.ArgumentsOf(made); for (int position = 0; position < arguments.Count; ++position) { ITypeSymbol argument = arguments[position]; if (!parameters.Any(parameter => SymbolEqualityComparer.Default.Equals(parameter, argument))) { return "argument " + (position + 1) + " of " + made.ToDisplayString() + " isn't one of the " + "converter's " + parameterKind + " type parameters"; } if (!used.Add(argument)) { return argument.Name + " is used more than once in " + made.ToDisplayString(); } } return parameters.Where(parameter => !used.Contains(parameter)) .Select(parameter => parameter.Name + " isn't an argument of " + made.ToDisplayString() + ", so nothing can fill it") .FirstOrDefault(); } private static string FamilyMismatchOf(INamedTypeSymbol converter, ITypeParameterSymbol subject) { string having = subject.Name + " has "; if (!SymbolEqualityComparer.Default.Equals(subject.ContainingSymbol, converter)) { return subject.Name + " isn't a type parameter of the converter itself"; } // An unmanaged constraint implies a struct one, so it has to be asked about first. if (subject.HasReferenceTypeConstraint) { return having + "a class constraint"; } if (subject.HasUnmanagedTypeConstraint) { return having + "an unmanaged constraint"; } if (subject.HasValueTypeConstraint) { return having + "a struct constraint"; } if (subject.HasConstructorConstraint) { return having + "a new() constraint"; } if (subject.HasNotNullConstraint) { return having + "a notnull constraint"; } if (subject.ConstraintTypes.Length == 0) { return having + "no constraint, so it stands for every type"; } if (subject.ConstraintTypes.Length > 1) { return having + "more than one constraint"; } var familyBase = subject.ConstraintTypes[0] as INamedTypeSymbol; if (familyBase == null || (familyBase.TypeKind != TypeKind.Class && familyBase.TypeKind != TypeKind.Interface)) { return "the constraint of " + subject.Name + " isn't a class or an interface"; } List parameters = ParametersOf(converter); parameters.RemoveAll(parameter => SymbolEqualityComparer.Default.Equals(parameter, subject)); return ArgumentMismatchOf(parameters, familyBase, parameterKind: "other"); } /// /// Returns what stops a generic of from being /// matched, worded to finish "but ...", or null if nothing does. /// public static string MismatchOf(INamedTypeSymbol converter, ITypeSymbol converted) { var subject = converted as ITypeParameterSymbol; if (subject != null) { return FamilyMismatchOf(converter, subject); } var named = converted as INamedTypeSymbol; if (named == null || !named.IsGenericType) { return converted.ToDisplayString() + " isn't a generic type"; } List parameters = ParametersOf(converter); int argumentCount = SaveableTypes.ArgumentsOf(named).Count; if (argumentCount != parameters.Count) { return "the converter's type parameters (" + parameters.Count + ") and the arguments of " + named.ToDisplayString() + " (" + argumentCount + ") differ in number"; } return ArgumentMismatchOf(parameters, named, parameterKind: "own"); } /// /// Returns true if is marked [Saveable]. /// public static bool IsMarkedSaveable(ITypeParameterSymbol parameter) { return parameter.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == SaveableClass.AttributeName); } /// /// Returns the positions, among the type arguments of (those of the types it's /// nested in first), that a generic of it marks [Saveable]. A type argument /// at any other position can be anything. For a family, they are the positions among the type arguments of /// the base class it saves every derived type of. /// public static EquatableArray SaveablePositions(INamedTypeSymbol converter, ITypeSymbol converted) { var named = converted as INamedTypeSymbol; INamedTypeSymbol familyBase = FamilyBaseOf(converter, converted); if (familyBase != null) { named = familyBase; } else if (named == null || !converter.IsGenericType || !MatchesShape(converter, converted)) { return default; } ImmutableArray.Builder returnMe = ImmutableArray.CreateBuilder(); List arguments = SaveableTypes.ArgumentsOf(named); for (int i = 0; i < arguments.Count; ++i) { var parameter = arguments[i] as ITypeParameterSymbol; if (parameter != null && IsMarkedSaveable(parameter)) { returnMe.Add(i); } } return new EquatableArray(returnMe.ToImmutable()); } /// /// Starts what a converter of a family is counted as converting. No type is spelled this way, so a family /// is never mistaken for the converter of its base class. /// public const string FamilyConvertedName = "type derived from "; /// /// Starts what a converter of an interface family is counted as converting. /// public const string InterfaceFamilyConvertedName = "type implementing "; /// /// Returns how the base class or interface a family converter saves the types of is named when matched /// against the types something has a converter for, or null if isn't a family /// converter. /// public static string FamilyBaseNameOf(INamedTypeSymbol converter, ITypeSymbol converted) { INamedTypeSymbol familyBase = FamilyBaseOf(converter, converted); if (familyBase == null) { return null; } return SaveableTypes.NameOf(familyBase.OriginalDefinition); } /// /// Returns the name of the assembly that defines the base class or interface a family converter saves the /// types of, or null if isn't a family converter. /// public static string FamilyBaseAssemblyOf(INamedTypeSymbol converter, ITypeSymbol converted) { INamedTypeSymbol familyBase = FamilyBaseOf(converter, converted); if (familyBase == null) { return null; } return familyBase.ContainingAssembly.Name; } /// /// Returns true if is a family converter for an interface. /// public static bool IsInterfaceFamily(INamedTypeSymbol converter, ITypeSymbol converted) { INamedTypeSymbol familyBase = FamilyBaseOf(converter, converted); return familyBase != null && familyBase.TypeKind == TypeKind.Interface; } /// /// Returns the name of each interface the interface of a family converter extends, which tells a more derived /// interface family from the one it overrides. It's empty if the converter isn't for an interface. /// public static EquatableArray ExtendedInterfacesOf(INamedTypeSymbol converter, ITypeSymbol converted) { INamedTypeSymbol familyBase = FamilyBaseOf(converter, converted); if (familyBase == null || familyBase.TypeKind != TypeKind.Interface) { return default; } return new EquatableArray(familyBase.AllInterfaces .Select(extended => SaveableTypes.NameOf(extended.OriginalDefinition)).Distinct().ToImmutableArray()); } /// /// Returns what is counted as converting when finding two that convert the same /// type. A generic converter counts as converting the generic type itself, not its own type parameters. /// public static string ConvertedNameOf(INamedTypeSymbol converter, ITypeSymbol converted) { string family = FamilyBaseNameOf(converter, converted); if (family != null) { if (IsInterfaceFamily(converter, converted)) { return InterfaceFamilyConvertedName + family; } return FamilyConvertedName + family; } if (converter.IsGenericType && MatchesShape(converter, converted)) { return SaveableTypes.NameOf(converted.OriginalDefinition); } return SaveableTypes.NameOf(converted); } /// /// Returns each type names with [assembly: SavedWith], in the order it does. /// public static List NamedBySavedWith(IAssemblySymbol assembly) { var returnMe = new List(); foreach (AttributeData attribute in assembly.GetAttributes()) { if (attribute.AttributeClass?.ToDisplayString() != SaveableTypes.SavedWithAttributeName || attribute.ConstructorArguments.Length != 1) { continue; } var converter = attribute.ConstructorArguments[0].Value as INamedTypeSymbol; if (converter != null) { returnMe.Add(converter.OriginalDefinition); } } return returnMe; } private static bool ConvertsABuiltIn(INamedTypeSymbol converter, ITypeSymbol converted) { if (converter.IsGenericType) { return SaveableTypes.IsBuiltInGeneric(converted); } return SaveableTypes.IsBuiltIn(converted) || SaveableTypes.IsBuiltInCollection(converted); } private static bool CanCreate(INamedTypeSymbol type) { return SymbolSearch.IsPublic(type) && type.InstanceConstructors.Any(constructor => constructor.Parameters.Length == 0 && constructor.DeclaredAccessibility == Accessibility.Public); } // Only an assembly that references Vellum can name a converter, which keeps the scan off the BCL. private static bool ReferencesVellum(IAssemblySymbol assembly, IAssemblySymbol vellum) { return assembly.Modules.Any(module => module.ReferencedAssemblySymbols.Contains(vellum, SymbolEqualityComparer.Default)); } private static int Compare(ConverterModel left, ConverterModel right) { int returnMe = string.CompareOrdinal(left.AssemblyName, right.AssemblyName); if (returnMe == 0) { returnMe = string.CompareOrdinal(left.MetadataName, right.MetadataName); } if (returnMe == 0) { returnMe = string.CompareOrdinal(left.FilePath, right.FilePath); } if (returnMe == 0) { returnMe = left.Span.Start.CompareTo(right.Span.Start); } return returnMe; } // Compares names only, which is enough to rule out nearly every class that has a base list. private static bool MightDeriveFromConverter(INamedTypeSymbol type) { for (INamedTypeSymbol baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) { if (baseType.MetadataName == ConverterBaseName) { return true; } } return false; } public static bool MightConvert(SyntaxNode node, CancellationToken cancellation) { var declaration = node as ClassDeclarationSyntax; return declaration != null && declaration.BaseList != null; } /// /// Returns as a converter, or null if it isn't one. /// public static ConverterModel Describe(SemanticModel semantics, ClassDeclarationSyntax declaration, CancellationToken cancellation) { var type = (INamedTypeSymbol)semantics.GetDeclaredSymbol(declaration, cancellation); // Looking the converter base up searches every reference, which is too much to do for each class. INamedTypeSymbol converterBase = null; if (MightDeriveFromConverter(type)) { converterBase = semantics.Compilation.GetTypeByMetadataName(ConverterMetadataName); } if (converterBase == null) { return null; } ITypeSymbol converted = ConvertedBy(type, converterBase); if (converted == null) { return null; } ConverterProblem problem = ConverterProblem.None; string problemDetail = null; if (!CanCreate(type)) { problem = ConverterProblem.Unusable; } else if (converted.TypeKind == TypeKind.Interface) { problem = ConverterProblem.Interface; } else if (ConvertsABuiltIn(type, converted)) { problem = ConverterProblem.BuiltIn; } else if (type.IsGenericType && !MatchesShape(type, converted)) { problem = ConverterProblem.Unmatchable; problemDetail = MismatchOf(type, converted) ?? "its converted type has a shape Vellum can't match"; } return new ConverterModel(type.ContainingAssembly.Name, ConverterModel.MetadataNameOf(type), type.Name, ConvertedNameOf(type, converted), problem, declaration.SyntaxTree.FilePath, declaration.Identifier.Span, declaration.Identifier.GetLocation().GetLineSpan().Span, FamilyBaseNameOf(type, converted), SaveablePositions(type, converted), IsInterfaceFamily(type, converted), ExtendedInterfacesOf(type, converted), FamilyBaseAssemblyOf(type, converted), problemDetail ); } /// /// Returns the converter a syntax node declares, or null if it declares none. /// public static ConverterModel Local(GeneratorSyntaxContext context, CancellationToken cancellation) { return Describe(context.SemanticModel, (ClassDeclarationSyntax)context.Node, cancellation); } // A library's own generator turned down a generic converter that doesn't match, so it's not named. private static void AddNamedBy(IAssemblySymbol assembly, INamedTypeSymbol converterBase, ImmutableArray.Builder models) { foreach (INamedTypeSymbol converter in NamedBySavedWith(assembly)) { ITypeSymbol converted = ConvertedBy(converter, converterBase); if (converted != null && (!converter.IsGenericType || MatchesShape(converter, converted))) { models.Add(new ConverterModel(converter.ContainingAssembly.Name, ConverterModel.MetadataNameOf(converter), converter.Name, ConvertedNameOf(converter, converted), ConverterProblem.None, filePath: null, span: default, lineSpan: default, familyBase: FamilyBaseNameOf(converter, converted), saveablePositions: SaveablePositions(converter, converted), familyIsInterface: IsInterfaceFamily(converter, converted), familyExtends: ExtendedInterfacesOf(converter, converted), familyBaseAssembly: FamilyBaseAssemblyOf(converter, converted) )); } } } /// /// Returns each converter the referenced assemblies name with [SavedWith]. /// public static EquatableArray Referenced(Compilation compilation) { var returnMe = ImmutableArray.CreateBuilder(); INamedTypeSymbol converterBase = compilation.GetTypeByMetadataName(ConverterMetadataName); if (converterBase == null) { return new EquatableArray(returnMe.ToImmutable()); } foreach (IAssemblySymbol assembly in compilation.SourceModule.ReferencedAssemblySymbols) { if (ReferencesVellum(assembly, converterBase.ContainingAssembly)) { AddNamedBy(assembly, converterBase, returnMe); } } return new EquatableArray(returnMe.ToImmutable()); } /// /// Returns the converters found in this compilation and its references, split into the ones that can be used /// and the ones to report. A converter is counted once however many assemblies name it. /// public static ConverterList Combine(EquatableArray local, EquatableArray referenced) { var all = new List(local.Items); all.AddRange(referenced.Items); all.Sort(Compare); var distinct = new List(); var convertedCounts = new Dictionary(); foreach (ConverterModel model in all) { if (distinct.Count > 0 && distinct[distinct.Count - 1].AssemblyName == model.AssemblyName && distinct[distinct.Count - 1].MetadataName == model.MetadataName) { continue; } distinct.Add(model); if (model.Problem == ConverterProblem.None) { int count; convertedCounts.TryGetValue(model.ConvertedName, out count); convertedCounts[model.ConvertedName] = count + 1; } } var usable = ImmutableArray.CreateBuilder(); var problems = ImmutableArray.CreateBuilder(); foreach (ConverterModel model in distinct) { if (model.Problem != ConverterProblem.None) { problems.Add(model); } else if (convertedCounts[model.ConvertedName] > 1) { problems.Add(model.WithProblem(ConverterProblem.Duplicate)); } else { usable.Add(model); } } return new ConverterList(new EquatableArray(usable.ToImmutable()), new EquatableArray(problems.ToImmutable())); } /// /// Returns the converters the compilation's own assembly and its references name with [SavedWith], which /// includes the ones the generator wrote for the compilation's [Saveable] types. Reading the attributes /// rather than the source is what lets an analyzer see generated converters. /// public static ConverterList FromSavedWith(Compilation compilation) { var own = ImmutableArray.CreateBuilder(); INamedTypeSymbol converterBase = compilation.GetTypeByMetadataName(ConverterMetadataName); if (converterBase != null) { AddNamedBy(compilation.Assembly, converterBase, own); } return Combine(new EquatableArray(own.ToImmutable()), Referenced(compilation)); } } }