/***************************************************************************************************
Copyright (C) 2025 The Qt Company Ltd.
SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
***************************************************************************************************/
global using Rules = Qt.DotNet.CodeGeneration.Rule.All;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace Test_Qt.DotNet.Generator.Support
{
using Qt.DotNet.CodeGeneration;
using Qt.DotNet.CodeGeneration.MetaFunctions;
using Qt.DotNet.CodeGeneration.Rules.Class;
///
/// Generates C# code from input sources by compiling them into a temporary
/// assembly, analyzing dependencies, and applying code generation rules.
///
public static class TestCodeGenerator
{
private static readonly string NewLine = Environment.NewLine;
public sealed record Result(DependencyGraph Graph, MetadataLoadContext Loader,
Assembly SourceAssembly, MemorySink Sink, string TargetDir)
{
/// Combines all generated files into a single string.
public string CombinedText => string.Join(NewLine + NewLine, Sink.Files.Values);
}
///
/// Compiles the provided C# sources into a temporary assembly, runs the code generator,
/// and captures the output in memory.
///
/// C# source files to compile.
/// List of reference assemblies
/// Additional directories to search for assembly references.
/// Aliased references (extern alias support).
/// Array of custom none build-in rules to apply.
/// Cancellation token.
/// Generated code and metadata.
public static async Task GenerateAsync(string[] sources,
Assembly[] sourceRefs = null, string[] extraRefs = null,
List<(string Alias, string Path)> referencesWithAliases = null,
Type[] extraRules = null,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(sources.ToString(), nameof(sources));
// Ensure no trace left from a previous run
Placeholder.ResetIndex();
Rules.Reset();
FilePlaceholder.All.Reset();
// Build up necessary dependencies infrastructure
var refs = CreateDefaultFrameworkPaths()
.Select(path => MetadataReference.CreateFromFile(path))
.Cast().ToList();
// Codegen infrastructure + user-specified sourceRefs
var assemblies = (sourceRefs ?? [])
.Union(
[
typeof(Rule).Assembly,
typeof(GenerateIndexer).Assembly,
typeof(BasicTypes).Assembly
])
.Distinct();
refs.AddRange(assemblies
.Select(assembly => MetadataReference.CreateFromFile(assembly.Location)));
// Add aliases if provided and update assembly references
if (referencesWithAliases is { Count: > 0 }) {
foreach (var (alias, path) in referencesWithAliases) {
refs.Add(MetadataReference.CreateFromFile(path,
new MetadataReferenceProperties(aliases: ["global", alias])));
}
var probe = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var p in extraRefs ?? []) {
if (!string.IsNullOrWhiteSpace(p) && Directory.Exists(p))
probe.Add(p);
}
foreach (var (_, path) in referencesWithAliases) {
var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(dir))
probe.Add(dir);
}
extraRefs = probe.ToArray();
}
// Compile input sources into a temporary assembly
var assemblyName = "CodeGeneratorTest_" + Guid.NewGuid().ToString("N");
var trees = sources.Select(src => CSharpSyntaxTree.ParseText(src)).ToArray();
var compilation = CSharpCompilation.Create(assemblyName , trees, refs,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var outputPath = Path.Combine(Path.GetTempPath(), assemblyName + ".dll");
var emitResult = compilation.Emit(outputPath, cancellationToken: ct);
if (!emitResult.Success) {
throw new InvalidOperationException("Emitting the test library failed:" + NewLine
+ string.Join(NewLine, emitResult.Diagnostics.Select(d => d.ToString())));
}
// Set up metadata loading context similar to codegen
var extraDirectories = new List {
RuntimeEnvironment.GetRuntimeDirectory(),
AppContext.BaseDirectory,
Path.GetDirectoryName(outputPath),
};
if (extraRefs is { Length: > 0 })
extraDirectories.AddRange(extraRefs.Where(Directory.Exists));
// Ensure Qt.DotNet.Adapter.dll is present among scanned dirs
var allDlls = extraDirectories
.SelectMany(d => Directory.EnumerateFiles(d, "*.dll"))
.Distinct(StringComparer.OrdinalIgnoreCase);
var hasAdapter = allDlls.Any(p => string.Equals(Path.GetFileNameWithoutExtension(p),
"Qt.DotNet.Adapter", StringComparison.OrdinalIgnoreCase));
if (!hasAdapter)
throw new InvalidOperationException("Qt.DotNet.Adapter.dll not found.");
// Create MLC using shared helper
var metadataLoadContext = MetadataResolver.CreateLoadContext(extraDirectories);
var sourceAssembly = metadataLoadContext.LoadFromAssemblyPath(outputPath);
// Register meta-functions and rules
MetaFunction.Register();
foreach (var t in typeof(GenerateIndexer).Assembly.ExportedTypes)
_ = t.TryRegisterAsRule() || t.TryRegisterAsMetaFunction();
// Register additional none build-in rules
foreach (var t in extraRules ?? [])
_ = t.TryRegisterAsRule();
// 4. Build dependency graph and run rules
await DependencyGraph.CreateAsync(metadataLoadContext, sourceAssembly,
Array.Empty());
var targetDirectory = Path.Combine(Path.GetTempPath(), "qtdotnet_codegen_" + Guid
.NewGuid().ToString("N"));
Directory.CreateDirectory(targetDirectory);
var rulesSucceeded = await Rules.RunAllAsync(targetDirectory);
if (!rulesSucceeded) {
var messages = Rules.Results.Where(result => !result.Succeeded)
.Select(result => result.Message);
throw new InvalidOperationException(
$"Running generation rules failed. Error: {string.Join("\r\n", messages)}");
}
// Capture outputs in memory
var sink = new MemorySink();
await FilePlaceholder.All.WriteAllAsync(sink, ct);
return new Result(Rules.SourceGraph, metadataLoadContext, Assembly.LoadFile(outputPath), sink,
targetDirectory);
}
private static IEnumerable CreateDefaultFrameworkPaths()
{
var tpa = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))!
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
var system = new[]
{
"System.Private.CoreLib.dll",
"System.Runtime.dll",
"System.Linq.dll",
"System.Console.dll",
"System.Collections.dll",
"System.Runtime.Extensions.dll",
"netstandard.dll"
};
var found = system.Select(name =>
{
return tpa.FirstOrDefault(p => string.Equals(Path.GetFileName(p), name,
StringComparison.OrdinalIgnoreCase));
})
.Where(p => p is not null).ToArray();
return found.Length == 0 ? [typeof(object).Assembly.Location] : found;
}
}
}