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