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