using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Ariadne.Generator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Emit;
namespace Vellum.Generator.UnitTests
{
///
/// Compiles source in memory the way a build with Vellum.Generator does, optionally with Ariadne.Generator too.
///
public class VellumRun
{
///
/// The usings put ahead of every source file.
///
public const string Usings = "using System.Collections.Generic;\nusing Vellum;\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(SaveNode).Assembly.Location),
};
///
/// Holds Ariadne and the bridge that saves its arcs with Vellum, for compiling arcs.
///
public static readonly MetadataReference[] ArcReferences =
{
MetadataReference.CreateFromFile(typeof(Ariadne.Arc<>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Ariadne.Vellum.ArcRecord).Assembly.Location),
};
///
/// The generators' own diagnostics, each as "Id:the text it points at".
///
public readonly List Reported;
///
/// The compiler's and analyzers' errors and unsuppressed warnings, each as "Id:the text it points at".
///
public readonly List Compiled;
///
/// The same diagnostics as Compiled, each as "Id:File(line,column)" where #line maps it.
///
public readonly List CompiledAt;
///
/// The same diagnostics as Compiled, each as "Id:its message".
///
public readonly List CompiledMessages;
///
/// The generated file naming the converters found, or "" if there were none.
///
public readonly string SavedWith;
///
/// The generated file naming the generic methods that save a type argument, or "" if there were none.
///
public readonly string SaveMethodsFile;
///
/// Each [Saveable] type's generated file, by file name.
///
public readonly Dictionary Saveables;
private VellumRun(List reported, List compiled, string savedWith,
string saveMethodsFile, Dictionary saveables)
{
Reported = reported;
Compiled = compiled.Select(Describe).ToList();
CompiledAt = compiled.Select(DescribeWhere).ToList();
CompiledMessages = compiled.Select(diagnostic => diagnostic.Id + ":" + diagnostic.GetMessage()).ToList();
SavedWith = savedWith;
SaveMethodsFile = saveMethodsFile;
Saveables = saveables;
}
///
/// Gets the only [Saveable] type's generated file, or "" if there's none.
///
public string Saveable
{
get
{
return Saveables.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);
}
///
/// Describes , finding the text a generator pointed at by file path in
/// .
///
public static string Describe(Diagnostic diagnostic, Compilation compilation)
{
Location location = diagnostic.Location;
if (location.Kind != LocationKind.ExternalFile)
{
return Describe(diagnostic);
}
SyntaxTree tree = compilation.SyntaxTrees.Single(candidate =>
candidate.FilePath == location.GetLineSpan().Path);
return diagnostic.Id + ":" + tree.GetText().ToString(location.SourceSpan);
}
///
/// Returns with the files Vellum.Generator writes for it added.
///
public static Compilation Generated(Compilation compilation)
{
Compilation generated;
ImmutableArray reported;
CSharpGeneratorDriver.Create(new[] { new SaveGenerator().AsSourceGenerator() }, parseOptions: ParseOptions)
.RunGeneratorsAndUpdateCompilation(compilation, out generated, out reported);
return generated;
}
///
/// Returns the converters of , found the way the generator finds them.
///
public static ConverterList Converters(Compilation compilation)
{
var local = ImmutableArray.CreateBuilder();
foreach (SyntaxTree tree in compilation.SyntaxTrees)
{
SemanticModel semantics = compilation.GetSemanticModel(tree);
foreach (ClassDeclarationSyntax declaration in
tree.GetRoot().DescendantNodes().OfType())
{
ConverterModel model = ConverterScan.Describe(semantics, declaration, CancellationToken.None);
if (model != null)
{
local.Add(model);
}
}
}
return ConverterScan.Combine(new EquatableArray(local.ToImmutable()),
ConverterScan.Referenced(compilation));
}
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)
{
return diagnostics
.OrderBy(diagnostic => diagnostic.Location.GetLineSpan().Path, StringComparer.Ordinal)
.ThenBy(diagnostic => diagnostic.Location.SourceSpan.Start)
.ThenBy(diagnostic => diagnostic.Id, StringComparer.Ordinal)
.ToList();
}
///
/// Compiles , each a path and source with Usings put ahead of it.
///
public static CSharpCompilation Compile(MetadataReference[] libraries,
params (string Path, string Source)[] files)
{
var trees = new List();
foreach ((string path, string source) in files)
{
trees.Add(CSharpSyntaxTree.ParseText(Usings + source, ParseOptions, path));
}
return CSharpCompilation.Create("Saving", trees, References.Concat(libraries),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
public static CSharpCompilation Compile(params (string Path, string Source)[] files)
{
return Compile(new MetadataReference[0], files);
}
///
/// Compiles into an assembly other source can reference, eg: a game's bridge
/// assembly of converters.
///
/// False builds the library as a project that never ran Vellum.Generator.
/// Other libraries the source can use.
/// The source doesn't compile.
public static MetadataReference CompileLibrary(string name, string source, bool runGenerator = true,
params MetadataReference[] libraries)
{
Compilation generated = CSharpCompilation.Create(name,
new[] { CSharpSyntaxTree.ParseText(Usings + source, ParseOptions) }, References.Concat(libraries),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
if (runGenerator)
{
ImmutableArray reported;
CSharpGeneratorDriver.Create(new[] { new SaveGenerator().AsSourceGenerator() },
parseOptions: ParseOptions).RunGeneratorsAndUpdateCompilation(generated, out generated,
out reported);
}
using (var image = new MemoryStream())
{
EmitResult result = generated.Emit(image);
if (!result.Success)
{
throw new InvalidOperationException(string.Join("\n", result.Diagnostics));
}
return MetadataReference.CreateFromImage(image.ToArray());
}
}
/// Runs Ariadne.Generator as well, as a project with arcs in it does.
public static VellumRun Of(MetadataReference[] libraries, bool withAriadne,
params (string Path, string Source)[] files)
{
var generators = new List { new SaveGenerator().AsSourceGenerator() };
var analyzers = new List { new SavedTypeAnalyzer() };
if (withAriadne)
{
generators.Add(new ArcScriptGenerator().AsSourceGenerator());
analyzers.Add(new StepLabelSuppressor());
}
GeneratorDriver driver = CSharpGeneratorDriver.Create(generators, parseOptions: ParseOptions);
Compilation generated;
ImmutableArray reported;
driver = driver.RunGeneratorsAndUpdateCompilation(Compile(libraries, files), out generated,
out reported);
List compiled = generated.WithAnalyzers(analyzers.ToImmutableArray())
.GetAllDiagnosticsAsync().Result
.Where(diagnostic => diagnostic.Severity >= DiagnosticSeverity.Warning && !diagnostic.IsSuppressed)
.ToList();
string savedWith = "";
string saveMethodsFile = "";
var saveables = new Dictionary(StringComparer.Ordinal);
foreach (GeneratedSourceResult source in driver.GetRunResult().Results
.SelectMany(result => result.GeneratedSources))
{
if (source.HintName == SaveGenerator.SavedWithFileName)
{
savedWith = source.SourceText.ToString();
}
else if (source.HintName == SaveMethods.FileName)
{
saveMethodsFile = source.SourceText.ToString();
}
else if (source.HintName.EndsWith(".Saveable.g.cs", StringComparison.Ordinal))
{
saveables.Add(source.HintName, source.SourceText.ToString());
}
}
return new VellumRun(Sorted(reported).Select(diagnostic => Describe(diagnostic, generated)).ToList(),
Sorted(compiled), savedWith, saveMethodsFile, saveables);
}
public static VellumRun Of(string source)
{
return Of(new MetadataReference[0], withAriadne: false, files: ("Source.cs", source));
}
public static VellumRun Of(MetadataReference library, string source)
{
return Of(new[] { library }, withAriadne: false, files: ("Source.cs", source));
}
///
/// Runs both generators on , which can use Ariadne and Ariadne.Vellum.
///
public static VellumRun OfArcs(string script)
{
return Of(ArcReferences, withAriadne: true, files: ("Script.cs", script));
}
}
}