Files
RobustToolbox/Robust.Analyzers.Tests/HasDependenciesGeneratorTest.cs
T
b4eb85ad3c [Dependency] source generator (#6549)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Release notes

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:02 +02:00

397 lines
13 KiB
C#

extern alias SerializationGenerator;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using NUnit.Framework;
using Robust.Analyzers.Generators;
namespace Robust.Analyzers.Tests;
[TestFixture]
[TestOf(typeof(HasDependenciesGenerator))]
[Parallelizable(ParallelScope.All)]
public sealed class HasDependenciesGeneratorTest
{
[Test]
public void TestBasic()
{
var result = RunGenerator("""
using Robust.Shared.IoC;
public sealed partial class Foobar
{
[Dependency]
public string Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
public partial class Foobar : global::Robust.Shared.IoC.IHasDependencies
{
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
{
return new global::System.Type[]
{
typeof(global::string)
};
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
{
Foo = (global::string)instances[0];
}
}
""");
}
[Test]
public void TestInheritGeneric()
{
var result = RunGenerator("""
using Robust.Shared.IoC;
public partial class Foo<T>
{
[Dependency] string _x = null!;
}
public sealed partial class Bar : Foo<int>
{
[Dependency]
public string _heck = null!;
}
""");
ExpectNoDiagnostics(result);
Assert.That(result.GeneratedSources, Has.Length.EqualTo(2));
ExpectNamedSource(
result,
"Foo`1.g.cs",
"""
// <auto-generated />
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
public partial class Foo<T> : global::Robust.Shared.IoC.IHasDependencies
{
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
{
return GetDependencyTypesImpl();
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
{
InjectImpl(instances);
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected virtual global::System.Type[] GetDependencyTypesImpl()
{
return new global::System.Type[]
{
typeof(global::string)
};
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected virtual void InjectImpl(global::System.ReadOnlySpan<object> instances)
{
_x = (global::string)instances[0];
}
}
""");
ExpectNamedSource(
result,
"Bar.g.cs",
"""
// <auto-generated />
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
public partial class Bar
{
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override global::System.Type[] GetDependencyTypesImpl()
{
var baseTypes = base.GetDependencyTypesImpl();
var types = new global::System.Type[baseTypes.Length + 1];
types[0] = typeof(global::string);
global::System.Array.Copy(baseTypes, 0, types, 1, baseTypes.Length);
return types;
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void InjectImpl(global::System.ReadOnlySpan<object> instances)
{
_heck = (global::string)instances[0];
base.InjectImpl(instances.Slice(1));
}
}
""");
}
[Test]
public void TestGenericInherit()
{
var result = RunGenerator("""
using Robust.Shared.IoC;
public partial class Foo
{
[Dependency] string _x = null!;
}
public sealed partial class Bar<T> : Foo
{
[Dependency]
public string _heck = null!;
}
""");
ExpectNoDiagnostics(result);
Assert.That(result.GeneratedSources, Has.Length.EqualTo(2));
ExpectNamedSource(
result,
"Foo.g.cs",
"""
// <auto-generated />
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
public partial class Foo : global::Robust.Shared.IoC.IHasDependencies
{
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
{
return GetDependencyTypesImpl();
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
{
InjectImpl(instances);
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected virtual global::System.Type[] GetDependencyTypesImpl()
{
return new global::System.Type[]
{
typeof(global::string)
};
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected virtual void InjectImpl(global::System.ReadOnlySpan<object> instances)
{
_x = (global::string)instances[0];
}
}
""");
ExpectNamedSource(
result,
"Bar`1.g.cs",
"""
// <auto-generated />
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
public partial class Bar<T>
{
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override global::System.Type[] GetDependencyTypesImpl()
{
var baseTypes = base.GetDependencyTypesImpl();
var types = new global::System.Type[baseTypes.Length + 1];
types[0] = typeof(global::string);
global::System.Array.Copy(baseTypes, 0, types, 1, baseTypes.Length);
return types;
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
protected override void InjectImpl(global::System.ReadOnlySpan<object> instances)
{
_heck = (global::string)instances[0];
base.InjectImpl(instances.Slice(1));
}
}
""");
}
[Test]
public void TestReadOnly()
{
var result = RunGenerator("""
using Robust.Shared.IoC;
public sealed partial class Foobar
{
[Dependency]
public readonly string Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectNoSource(result);
}
[Test]
public void TestNotPartial()
{
var result = RunGenerator("""
using Robust.Shared.IoC;
public sealed class Foobar
{
[Dependency]
public string Foo;
}
""");
ExpectNoDiagnostics(result);
ExpectNoSource(result);
}
[Test]
public void TestNested()
{
var result = RunGenerator("""
using Robust.Shared.IoC;
public sealed partial class Real
{
public sealed partial class Foobar
{
[Dependency]
public string Foo;
}
}
""");
ExpectNoDiagnostics(result);
ExpectSource(
result,
"""
// <auto-generated />
public partial class Real
{
[global::Robust.Shared.IoC.HasDependenciesGeneratedAttribute]
public partial class Foobar : global::Robust.Shared.IoC.IHasDependencies
{
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
{
return new global::System.Type[]
{
typeof(global::string)
};
}
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan<object> instances)
{
Foo = (global::string)instances[0];
}
}
}
""");
}
private static void ExpectSource(GeneratorRunResult result, string expected)
{
Assert.That(result.GeneratedSources, Has.Length.EqualTo(1));
var source = result.GeneratedSources[0];
Assert.That(source.SourceText.ToString().ReplaceLineEndings(), Is.EqualTo(expected.ReplaceLineEndings()));
}
private static void ExpectNamedSource(GeneratorRunResult result, string name, string expected)
{
var source = result.GeneratedSources.Single(s => s.HintName == name);
Assert.That(source.SourceText.ToString().ReplaceLineEndings(), Is.EqualTo(expected.ReplaceLineEndings()));
}
private static void ExpectNoSource(GeneratorRunResult result)
{
Assert.That(result.GeneratedSources, Is.Empty);
}
private static void ExpectNoDiagnostics(GeneratorRunResult result)
{
Assert.That(result.Diagnostics, Is.Empty);
}
private static void ExpectDiagnostics(GeneratorRunResult result, (string code, LinePositionSpan span)[] diagnostics)
{
Assert.Multiple(() =>
{
Assert.That(result.Diagnostics, Has.Length.EqualTo(diagnostics.Length));
foreach (var (code, span) in diagnostics)
{
Assert.That(
result.Diagnostics.Any(x => x.Id == code && x.Location.GetLineSpan().Span == span),
$"Expected diagnostic with code {code} and location {span}");
}
});
}
private static GeneratorRunResult RunGenerator(string source)
{
var compilation = (Compilation)CSharpCompilation.Create("compilation",
[
CSharpSyntaxTree.ParseText(source, path: "Source.cs"),
..TestHelper.GetEmbeddedSyntaxTrees(
"Robust.Shared.IoC.DependencyAttribute.cs",
"Robust.Shared.IoC.IHasDependencies.cs"),
],
new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var generator = new HasDependenciesGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _);
var result = driver.GetRunResult();
return result.Results[0];
}
}