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