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
{
///
/// Writes a SaveConverter for each [Saveable] type, checks every converter the compilation can use, and names
/// them all with [assembly: SavedWith] so the runtime finds the same ones. Also names each generic method that
/// saves a type built from its type parameters with [assembly: RegisterSaveMethod].
///
[Generator(LanguageNames.CSharp)]
public class SaveGenerator : IIncrementalGenerator
{
public const string SavedWithFileName = "Vellum.SavedWith.g.cs";
public const string LocalConvertersStep = "LocalConverters";
public const string ReferencedConvertersStep = "ReferencedConverters";
public const string ConvertersStep = "Converters";
public const string SaveableModelsStep = "SaveableModels";
public const string SaveableShapesStep = "SaveableShapes";
public const string SaveableChecksStep = "SaveableChecks";
public const string GeneratedConverterNamesStep = "GeneratedConverterNames";
public const string BaseListedTypesStep = "BaseListedTypes";
public const string InheritorsStep = "Inheritors";
public const string UnsavedSubtypesStep = "UnsavedSubtypes";
public const string SaveableMarksStep = "SaveableMarks";
public const string FamilyMembersStep = "FamilyMembers";
public const string ReferencedFamilyMembersStep = "ReferencedFamilyMembers";
public const string SaveEdgesStep = "SaveEdges";
public const string SaveSeedsStep = "SaveSeeds";
///
/// Returns a file naming every converter with [SavedWith], hand-written and generated, or null if there are
/// none.
///
public static string SavedWithFile(ConverterList converters, EquatableArray generated)
{
List names = converters.Usable.Items.Select(converter => converter.TypeofName)
.Concat(generated.Items)
.OrderBy(name => name, System.StringComparer.Ordinal)
.ToList();
if (names.Count == 0)
{
return null;
}
var code = new CodeWriter();
code.AddLine("// ");
code.AddLine("// The save converters Vellum.Generator found, for this assembly to save with.");
foreach (string name in names)
{
code.AddLine("[assembly: global::Vellum.SavedWith(typeof(" + name + "))]");
}
return code.ToString();
}
// Reported alongside the compilation, which this step therefore reruns for on every change. A diagnostic
// needs a location in the compilation's own syntax tree to be suppressible with a #pragma.
private static void Report(SourceProductionContext production, Compilation compilation,
ConverterList converters, EquatableArray checks, EquatableArray subtypes,
EquatableArray marks, EquatableArray members)
{
foreach (ConverterModel problem in converters.Problems.Items)
{
production.ReportDiagnostic(problem.ToDiagnostic(compilation));
}
foreach (SaveableMark mark in marks.Items)
{
production.ReportDiagnostic(mark.ToDiagnostic(compilation));
}
HashSet converted = converters.ConvertedNames();
foreach (SaveableChecks type in checks.Items)
{
foreach (Diagnostic diagnostic in type.Check(compilation, converters, converted))
{
production.ReportDiagnostic(diagnostic);
}
}
Dictionary> families = converters.FamilyBaseNames();
foreach (UnsavedSubtype subtype in subtypes.Items)
{
foreach (Diagnostic diagnostic in subtype.Check(compilation, converted, families))
{
production.ReportDiagnostic(diagnostic);
}
}
Dictionary> interfaceFamilies = converters.InterfaceFamilies();
foreach (FamilyMember member in members.Items)
{
foreach (Diagnostic diagnostic in member.Check(compilation, families, interfaceFamilies))
{
production.ReportDiagnostic(diagnostic);
}
}
}
private static void WriteSaveable(SourceProductionContext production, SaveableShape shape)
{
production.AddSource(shape.FileName, SaveableWriter.Write(shape));
}
private static void WriteSavedWith(SourceProductionContext production,
(ConverterList Converters, EquatableArray Generated) input)
{
string savedWith = SavedWithFile(input.Converters, input.Generated);
if (savedWith != null)
{
production.AddSource(SavedWithFileName, savedWith);
}
}
// Mistakes in the source reach here too, eg: [Saveable] on a method or on two parts of a partial type.
// The compiler reports those. A generator that threw instead would hide everything else it writes.
// A type parameter marked [Saveable] reaches here too, and Describe ignores it.
private static SaveableMark DescribeMark(GeneratorAttributeSyntaxContext context,
CancellationToken cancellation)
{
var parameter = context.TargetSymbol as ITypeParameterSymbol;
INamedTypeSymbol converterBase = context.SemanticModel.Compilation.GetTypeByMetadataName(
ConverterScan.ConverterMetadataName);
if (parameter == null || converterBase == null)
{
return null;
}
return SaveableMark.Of(parameter, converterBase);
}
private static SaveableModel Describe(GeneratorAttributeSyntaxContext context, CancellationToken cancellation)
{
var type = context.TargetSymbol as INamedTypeSymbol;
if (type == null || !SaveableClass.IsFirstMark(type, context.TargetNode))
{
return null;
}
return SaveableClass.Describe(type);
}
// Only a class or struct that lists what it derives from or implements can inherit the mark. An interface
// that extends a marked one doesn't need anything generated.
private static bool MightInherit(SyntaxNode node, CancellationToken cancellation)
{
var declaration = node as TypeDeclarationSyntax;
return declaration != null && declaration.BaseList != null && !(node is InterfaceDeclarationSyntax);
}
// A type of several parts is described once, at the first part that lists a base. Both the inheritance and
// the interface family checks read it from here, so each type with a base list is bound only once.
private static BaseListedType DescribeBaseListed(GeneratorSyntaxContext context,
CancellationToken cancellation)
{
var type = context.SemanticModel.GetDeclaredSymbol(context.Node, cancellation) as INamedTypeSymbol;
if (type == null || !SaveableClass.IsFirstBaseList(type, context.Node))
{
return null;
}
return BaseListedType.Of(type);
}
private static void GenerateSaveMethods(SourceProductionContext production, ImmutableArray edges,
EquatableArray seeds)
{
var dropped = new List>();
List found = SaveMethods.Solve(seeds.Items, edges, dropped);
foreach (KeyValuePair problem in dropped)
{
production.ReportDiagnostic(Diagnostic.Create(VellumDiagnostics.UnregisterableSaveMethod,
Location.None, problem.Key, problem.Value));
}
string file = SaveMethods.File(SaveMethods.Attributes(found));
if (file != null)
{
production.AddSource(SaveMethods.FileName, file);
}
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider> local = context.SyntaxProvider
.CreateSyntaxProvider(ConverterScan.MightConvert, ConverterScan.Local)
.Where(converter => converter != null)
.WithTrackingName(LocalConvertersStep)
.Collect()
.Select((found, cancellation) => new EquatableArray(found));
IncrementalValueProvider> referenced = context.CompilationProvider
.Select((compilation, cancellation) => ConverterScan.Referenced(compilation))
.WithTrackingName(ReferencedConvertersStep);
IncrementalValueProvider converters = local.Combine(referenced)
.Select((pair, cancellation) => ConverterScan.Combine(pair.Left, pair.Right))
.WithTrackingName(ConvertersStep);
IncrementalValuesProvider marked = context.SyntaxProvider
.ForAttributeWithMetadataName(SaveableClass.AttributeName,
(node, cancellation) => true, Describe)
.Where(saveable => saveable != null);
IncrementalValuesProvider baseListed = context.SyntaxProvider
.CreateSyntaxProvider(MightInherit, DescribeBaseListed)
.Where(baseListedType => baseListedType != null)
.WithTrackingName(BaseListedTypesStep);
IncrementalValuesProvider inheritors = baseListed
.Select((baseListedType, cancellation) => baseListedType.Inheritor)
.Where(inheritor => inheritor != null)
.WithTrackingName(InheritorsStep);
IncrementalValueProvider> inherited = inheritors
.Select((inheritor, cancellation) => inheritor.Model)
.Where(model => model != null)
.Collect()
.Select((found, cancellation) => new EquatableArray(found));
IncrementalValuesProvider saveables = marked.Collect()
.Select((found, cancellation) => new EquatableArray(found))
.Combine(inherited)
.SelectMany((pair, cancellation) => pair.Left.Items.AddRange(pair.Right.Items))
.WithTrackingName(SaveableModelsStep);
IncrementalValueProvider> subtypes = inheritors
.Select((inheritor, cancellation) => inheritor.Subtype)
.Where(subtype => subtype != null)
.Collect()
.Select((found, cancellation) => new EquatableArray(found))
.WithTrackingName(UnsavedSubtypesStep);
IncrementalValuesProvider shapes = saveables
.Select((saveable, cancellation) => saveable.Shape)
.Where(shape => shape != null)
.WithTrackingName(SaveableShapesStep);
IncrementalValueProvider> checks = saveables
.Select((saveable, cancellation) => saveable.Checks)
.Collect()
.Select((found, cancellation) => new EquatableArray(found))
.WithTrackingName(SaveableChecksStep);
IncrementalValueProvider> generated = shapes
.Where(shape => !shape.IsAbstract)
.Select((shape, cancellation) => shape.ConverterFullName)
.Collect()
.Select((found, cancellation) => new EquatableArray(found))
.WithTrackingName(GeneratedConverterNamesStep);
IncrementalValueProvider> marks = context.SyntaxProvider
.ForAttributeWithMetadataName(SaveableClass.AttributeName,
(node, cancellation) => node is TypeParameterSyntax, DescribeMark)
.Where(mark => mark != null)
.Collect()
.Select((found, cancellation) => new EquatableArray(found))
.WithTrackingName(SaveableMarksStep);
IncrementalValueProvider> localMembers = baseListed
.Select((baseListedType, cancellation) => baseListedType.Member)
.Where(member => member != null)
.Collect()
.Select((found, cancellation) => new EquatableArray(found))
.WithTrackingName(FamilyMembersStep);
IncrementalValueProvider> referencedMembers = context.CompilationProvider
.Combine(converters)
.Select((pair, cancellation) => FamilyMember.Referenced(pair.Left, pair.Right))
.WithTrackingName(ReferencedFamilyMembersStep);
IncrementalValueProvider> members = localMembers.Combine(referencedMembers)
.Select((pair, cancellation) =>
new EquatableArray(pair.Left.Items.AddRange(pair.Right.Items)));
context.RegisterSourceOutput(shapes, WriteSaveable);
context.RegisterSourceOutput(
context.CompilationProvider
.Combine(converters)
.Combine(checks)
.Combine(subtypes)
.Combine(marks)
.Combine(members),
(production, input) => Report(production,
input.Left.Left.Left.Left.Left,
input.Left.Left.Left.Left.Right,
input.Left.Left.Left.Right,
input.Left.Left.Right,
input.Left.Right,
input.Right));
context.RegisterSourceOutput(converters.Combine(generated),
(production, input) => WriteSavedWith(production, (input.Left, input.Right)));
IncrementalValueProvider> edges = context.SyntaxProvider
.CreateSyntaxProvider(SaveMethods.MightHoldEdges, SaveMethods.FindMemberEdges)
.SelectMany((found, cancellation) => found)
.Collect()
.WithTrackingName(SaveEdgesStep);
IncrementalValueProvider> seeds = context.CompilationProvider
.Select((compilation, cancellation) => SaveMethods.SeedsOf(compilation))
.WithTrackingName(SaveSeedsStep);
context.RegisterSourceOutput(edges.Combine(seeds),
(production, input) => GenerateSaveMethods(production, input.Left, input.Right));
}
}
}