using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
namespace Ariadne.Generator.UnitTests
{
///
/// Compiles scripts in memory the way a build does: through ArcScriptGenerator, then StepLabelSuppressor.
///
public class GeneratorRun
{
///
/// This field holds the usings put ahead of every script file.
///
public const string Usings = "using System.Collections.Generic;\nusing System.Linq;\nusing Ariadne;\n\n";
///
/// This field holds the context type every test script runs against.
///
public const string World =
"public class World\n{\n public readonly System.Collections.Generic.HashSet Open = " +
"new System.Collections.Generic.HashSet();\n}\n";
public static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.CSharp13);
public static readonly MetadataReference[] References =
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Uri).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
MetadataReference.CreateFromFile(
Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "netstandard.dll")),
MetadataReference.CreateFromFile(typeof(Arc<>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Amphora.Duration).Assembly.Location),
};
///
/// This field holds the generator's own diagnostics, each as "Id:the text it points at".
///
public readonly List Reported;
///
/// Holds the same diagnostics as Reported, each as "Id:the message".
///
public readonly List Messages;
///
/// This field holds the compiler's errors and unsuppressed warnings with the generated code included, each as
/// "Id:the text it points at".
///
public readonly List Compiled;
///
/// This field holds the same diagnostics as Compiled, each as "Id:File(line,column)" where #line maps it.
///
public readonly List CompiledAt;
///
/// This field holds each generated code-behind's text, by file name.
///
public readonly Dictionary CodeBehinds;
private GeneratorRun(List reported, List messages, List compiled,
List compiledAt, Dictionary codeBehinds)
{
Reported = reported;
Messages = messages;
Compiled = compiled;
CompiledAt = compiledAt;
CodeBehinds = codeBehinds;
}
///
/// Gets the only code-behind, or "" if nothing was generated.
///
public string CodeBehind
{
get
{
return CodeBehinds.Values.SingleOrDefault() ?? "";
}
}
public static string Describe(Diagnostic diagnostic)
{
Location location = diagnostic.Location;
if (location.SourceTree == null)
{
return diagnostic.Id;
}
return diagnostic.Id + ":" + location.SourceTree.GetText().ToString(location.SourceSpan);
}
public static string DescribeWhere(Diagnostic diagnostic)
{
FileLinePositionSpan span = diagnostic.Location.GetMappedLineSpan();
return diagnostic.Id + ":" + Path.GetFileName(span.Path) + "(" + (span.StartLinePosition.Line + 1) + "," +
(span.StartLinePosition.Character + 1) + ")";
}
private static List Sorted(IEnumerable diagnostics, Func describe)
{
return diagnostics
.OrderBy(diagnostic => diagnostic.Location.SourceTree?.FilePath, StringComparer.Ordinal)
.ThenBy(diagnostic => diagnostic.Location.SourceSpan.Start)
.ThenBy(diagnostic => diagnostic.Id, StringComparer.Ordinal)
.Select(describe).ToList();
}
///
/// Compiles the World context type plus , each a path and a script with Usings put
/// ahead of it.
///
public static CSharpCompilation Compile(MetadataReference[] libraries,
params (string Path, string Script)[] files)
{
var trees = new List { CSharpSyntaxTree.ParseText(World, ParseOptions, "World.cs") };
foreach ((string path, string script) in files)
{
trees.Add(CSharpSyntaxTree.ParseText(Usings + script, ParseOptions, path));
}
return CSharpCompilation.Create("Scripts", trees, References.Concat(libraries),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
public static CSharpCompilation Compile(params (string Path, string Script)[] files)
{
return Compile(new MetadataReference[0], files);
}
///
/// Compiles , with Usings ahead of it, into an assembly a script can reference, eg:
/// a game's base class for its scripts.
///
/// The source doesn't compile.
public static MetadataReference CompileLibrary(string name, string source)
{
CSharpCompilation library = CSharpCompilation.Create(name,
new[] { CSharpSyntaxTree.ParseText(Usings + source, ParseOptions) },
References,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using (var image = new MemoryStream())
{
EmitResult result = library.Emit(image);
if (!result.Success)
{
throw new InvalidOperationException(string.Join("\n", result.Diagnostics));
}
return MetadataReference.CreateFromImage(image.ToArray());
}
}
///
/// Compiles as does, but through ArcScriptGenerator
/// first, so the assembly holds the passages and frames a project that uses the generator would build.
///
/// The source or its generated code doesn't compile.
public static MetadataReference CompileGeneratedLibrary(string name, string source)
{
GeneratorDriver driver = CSharpGeneratorDriver.Create(
new[] { new ArcScriptGenerator().AsSourceGenerator() }, parseOptions: ParseOptions,
optionsProvider: new ProjectOptions(null));
CSharpCompilation library = CSharpCompilation.Create(name,
new[] { CSharpSyntaxTree.ParseText(Usings + source, ParseOptions, name + ".cs") },
References,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
Compilation generated;
ImmutableArray reported;
driver.RunGeneratorsAndUpdateCompilation(library, out generated, out reported);
using (var image = new MemoryStream())
{
EmitResult result = generated.Emit(image);
if (!result.Success || reported.Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error))
{
throw new InvalidOperationException(string.Join("\n", reported.Concat(result.Diagnostics)));
}
return MetadataReference.CreateFromImage(image.ToArray());
}
}
///
/// Holds the build properties a build passes the compiler, which here is only where the project is.
///
public class ProjectOptions : AnalyzerConfigOptionsProvider
{
private class Options : AnalyzerConfigOptions
{
private readonly string projectDirectory;
public Options(string projectDirectory)
{
this.projectDirectory = projectDirectory;
}
public override bool TryGetValue(string key, out string value)
{
value = null;
if (key == ArcScriptGenerator.ProjectDirectoryOption && projectDirectory != null)
{
value = projectDirectory;
}
return value != null;
}
}
private readonly AnalyzerConfigOptions options;
public ProjectOptions(string projectDirectory)
{
options = new Options(projectDirectory);
}
public override AnalyzerConfigOptions GlobalOptions
{
get
{
return options;
}
}
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
{
return options;
}
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
{
return options;
}
}
///
/// Compiles in a project at , which is null for a
/// build that doesn't say.
///
public static GeneratorRun OfProject(string projectDirectory, params (string Path, string Script)[] files)
{
return Of(null, new MetadataReference[0], projectDirectory, files);
}
/// Another analyzer to run alongside, eg: a save library's, or null for none.
public static GeneratorRun Of(DiagnosticAnalyzer analyzer, MetadataReference[] libraries,
params (string Path, string Script)[] files)
{
return Of(analyzer, libraries, null, files);
}
private static GeneratorRun Of(DiagnosticAnalyzer analyzer, MetadataReference[] libraries,
string projectDirectory, (string Path, string Script)[] files)
{
GeneratorDriver driver = CSharpGeneratorDriver.Create(
new[] { new ArcScriptGenerator().AsSourceGenerator() }, parseOptions: ParseOptions,
optionsProvider: new ProjectOptions(projectDirectory));
Compilation generated;
ImmutableArray reported;
driver = driver.RunGeneratorsAndUpdateCompilation(Compile(libraries, files), out generated,
out reported);
var analyzers = new List { new StepLabelSuppressor() };
if (analyzer != null)
{
analyzers.Add(analyzer);
}
List compiled = generated
.WithAnalyzers(analyzers.ToImmutableArray())
.GetAllDiagnosticsAsync().Result
.Where(diagnostic => diagnostic.Severity >= DiagnosticSeverity.Warning && !diagnostic.IsSuppressed)
.ToList();
var codeBehinds = new Dictionary(StringComparer.Ordinal);
foreach (GeneratedSourceResult source in driver.GetRunResult().Results.Single().GeneratedSources)
{
codeBehinds.Add(source.HintName, source.SourceText.ToString());
}
return new GeneratorRun(Sorted(reported, Describe),
Sorted(reported, diagnostic => diagnostic.Id + ":" + diagnostic.GetMessage()), Sorted(compiled, Describe),
Sorted(compiled, DescribeWhere), codeBehinds);
}
public static GeneratorRun Of(MetadataReference[] libraries, params (string Path, string Script)[] files)
{
return Of(null, libraries, files);
}
public static GeneratorRun Of(params (string Path, string Script)[] files)
{
return Of(new MetadataReference[0], files);
}
public static GeneratorRun Of(string script)
{
return Of(("Script.cs", script));
}
///
/// Runs in a compilation that also references .
///
public static GeneratorRun Of(MetadataReference library, string script)
{
return Of(new[] { library }, ("Script.cs", script));
}
public static GeneratorRun Of(DiagnosticAnalyzer analyzer, string script)
{
return Of(analyzer, new MetadataReference[0], ("Script.cs", script));
}
}
}