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