using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Ariadne.Generator { /// /// Writes Script.CodeBehind.cs for every class that overrides Arc.Run. /// [Generator(LanguageNames.CSharp)] public class ArcScriptGenerator : IIncrementalGenerator { public const string ArcOutputsStep = "ArcOutputs"; public const string ArcCompositionStep = "ArcComposition"; public const string ArcFilesStep = "ArcFiles"; private const string ArcBaseName = "Arc`1"; private static bool IsDerivedClass(SyntaxNode node, CancellationToken cancellation) { var declaration = node as ClassDeclarationSyntax; return declaration != null && declaration.BaseList != null; } // Compares names only, which is enough to rule out nearly every class that has a base list. private static bool MightDeriveFromArc(INamedTypeSymbol type) { for (INamedTypeSymbol baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) { if (baseType.MetadataName == ArcBaseName) { return true; } } return false; } // A partial class is described once, from the first part that says what it derives from. private static bool IsFirstDerivedPart(INamedTypeSymbol type, SyntaxNode node) { return type.DeclaringSyntaxReferences .Select(reference => reference.GetSyntax()) .OfType() .First(declaration => declaration.BaseList != null) == node; } private static INamedTypeSymbol ArcTypeOf(GeneratorSyntaxContext context, CancellationToken cancellation) { var type = context.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)context.Node, cancellation) as INamedTypeSymbol; if (type == null || !MightDeriveFromArc(type) || !IsFirstDerivedPart(type, context.Node)) { return null; } INamedTypeSymbol arcDefinition = context.SemanticModel.Compilation .GetTypeByMetadataName(ScriptClass.ArcMetadataName); if (arcDefinition == null || ScriptClass.FindArcBase(type, arcDefinition) == null) { return null; } return type; } private static bool IsPartial(INamedTypeSymbol type) { return type.DeclaringSyntaxReferences .Select(reference => reference.GetSyntax()) .OfType() .All(declaration => declaration.Modifiers.Any(SyntaxKind.PartialKeyword)); } // The code-behind sits inside the containers, as another part of each. private static bool ReportUnpartialContainers(List problems, INamedTypeSymbol type) { bool returnMe = false; foreach (INamedTypeSymbol container in CodeBehindWriter.ContainersOf(type)) { if (!IsPartial(container)) { problems.Add(Diagnostic.Create(ScriptDiagnostics.NotPartialContainer, container.Locations.FirstOrDefault(), container.Name, type.Name)); returnMe = true; } } return returnMe; } // A class writing its own __SaveFields is hand-built, like one writing its own __Resume. private static List FieldsToSave(INamedTypeSymbol type) { if (SaveCodeWriter.HandBuilt(type)) { return new List(); } return SavedFields.Of(type); } // A back door anyone could call would let them build a half-loaded arc. private static bool ReportLoaderAccess(List problems, INamedTypeSymbol type) { IMethodSymbol loader = SaveCodeWriter.OwnLoader(type); Accessibility required = SaveCodeWriter.LoaderAccessibility(type); if (loader == null || loader.DeclaredAccessibility == required) { return false; } problems.Add(Diagnostic.Create(ScriptDiagnostics.WrongLoaderAccess, loader.Locations.First(), type.Name, required.ToString().ToLowerInvariant())); return true; } /// /// Returns the script is, or null after reporting why it can't be one. /// private static ScriptClass ScriptFor(List problems, Compilation compilation, INamedTypeSymbol type) { Location location = type.Locations.FirstOrDefault(); if (!IsPartial(type)) { problems.Add(Diagnostic.Create(ScriptDiagnostics.NotPartial, location, type.Name)); return null; } if (ReportUnpartialContainers(problems, type)) { return null; } // Turn down a Run without a block body. ScriptClass returnMe = ScriptClass.Create(type, compilation); if (returnMe == null) { IMethodSymbol run = type.GetMembers("Run").OfType() .FirstOrDefault(method => method.IsOverride); problems.Add(Diagnostic.Create(ScriptDiagnostics.PassageShape, run?.Locations.FirstOrDefault() ?? location, "Run")); } return returnMe; } /// /// Returns what to write for , or null if nothing, after reporting any problem. /// private static GeneratedArc ArcFor(List problems, Compilation compilation, INamedTypeSymbol arcDefinition, INamedTypeSymbol type) { if (ScriptClass.WritesOwnResume(type)) { return null; } List fields = FieldsToSave(type); SavedFields.ReportDuplicateKeys(type, arcDefinition, fields, problems); if (!SaveCodeWriter.HandBuilt(type)) { SavedFields.ReportUnsavableEvents(type, problems); } if (ReportLoaderAccess(problems, type)) { return null; } bool isScript = type.GetMembers("Run").OfType() .Any(method => ScriptClass.OverridesArcRun(method, arcDefinition)); if (isScript) { ScriptClass script = ScriptFor(problems, compilation, type); if (script == null) { return null; } return new GeneratedArc(type, script, fields); } if (SaveCodeWriter.HandBuilt(type)) { return null; } // A class with no Run can still hold passages, for the scripts that derive from it. ScriptClass host = ScriptClass.Create(type, compilation); if (host == null) { ScriptChecks.ReportPausesInNonScript(type, compilation, problems); } if (!IsPartial(type)) { problems.Add(Diagnostic.Create(ScriptDiagnostics.UnsupportedSavingClass, type.Locations.FirstOrDefault(), type.Name)); return null; } if (ReportUnpartialContainers(problems, type)) { return null; } return new GeneratedArc(type, host, fields); } private static EquatableArray DataOf(List diagnostics) { return new EquatableArray( diagnostics.Select(DiagnosticData.Of).ToImmutableArray()); } /// /// Returns what to write for the arc class around 's node, or null if the node isn't /// one, or there is nothing to write or report. /// public static ArcOutput Describe(GeneratorSyntaxContext context, CancellationToken cancellation) { INamedTypeSymbol type = ArcTypeOf(context, cancellation); if (type == null) { return null; } Compilation compilation = context.SemanticModel.Compilation; INamedTypeSymbol arcDefinition = compilation.GetTypeByMetadataName(ScriptClass.ArcMetadataName); var classProblems = new List(); GeneratedArc arc = ArcFor(classProblems, compilation, arcDefinition, type); SourcePlace place = SourcePlace.Of(type.Locations.FirstOrDefault()); if (arc == null) { if (classProblems.Count == 0) { return null; } return new ArcOutput(null, type.Name, place, new EquatableArray(), null, DataOf(classProblems), new EquatableArray()); } var scriptProblems = new List(); string classText = CodeBehindWriter.WriteClassOf(arc, scriptProblems); return new ArcOutput(arc.FilePath, type.Name, place, new EquatableArray(CodeBehindWriter.FileUsings(arc).ToImmutableArray()), classText, DataOf(classProblems), DataOf(scriptProblems)); } private static void Report(SourceProductionContext production, Compilation compilation, ArcComposition composition) { foreach (DiagnosticData diagnostic in composition.Diagnostics.Items) { production.ReportDiagnostic(diagnostic.ToDiagnostic(compilation)); } } /// /// This field holds the name of the build property that says where the project is. /// public const string ProjectDirectoryOption = "build_property.MSBuildProjectDirectory"; public void Initialize(IncrementalGeneratorInitializationContext context) { IncrementalValueProvider projectDirectory = context.AnalyzerConfigOptionsProvider .Select((options, cancellation) => { string directory; options.GlobalOptions.TryGetValue(ProjectDirectoryOption, out directory); return directory ?? ""; }); IncrementalValueProvider composition = context.SyntaxProvider .CreateSyntaxProvider(IsDerivedClass, Describe) .Where(output => output != null) .WithTrackingName(ArcOutputsStep) .Collect() .Combine(projectDirectory) .Select((input, cancellation) => CodeBehindWriter.Compose(new EquatableArray(input.Left), input.Right)) .WithTrackingName(ArcCompositionStep); context.RegisterSourceOutput( composition.SelectMany((found, cancellation) => found.Files.Items).WithTrackingName(ArcFilesStep), (production, file) => production.AddSource(file.FileName, file.Text)); // 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. context.RegisterSourceOutput(context.CompilationProvider.Combine(composition), (production, input) => Report(production, input.Left, input.Right)); } } }