From 10b40c0f2623a94a2e8adc51fd7bd1e0fd39a1b8 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:12:58 +0100 Subject: [PATCH] perf: limit converter discovery to declarations --- .../Generators/AotConverterGenerator.cs | 43 +++++++---- .../AotConverterScanTests.cs | 71 +++++++++++++++++++ 2 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 tests/TUnit.Core.SourceGenerator.Tests/AotConverterScanTests.cs diff --git a/src/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs b/src/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs index c0728b4dd0..8e84089348 100644 --- a/src/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs +++ b/src/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs @@ -107,6 +107,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) private void ScanTestParameters(Compilation compilation, List conversionInfos, CancellationToken cancellationToken) { var typesToScan = new HashSet(SymbolEqualityComparer.Default); + var baseTestAttribute = compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.BaseTestAttribute.WithoutGlobalPrefix); foreach (var tree in compilation.SyntaxTrees) { @@ -115,19 +116,26 @@ private void ScanTestParameters(Compilation compilation, List co var semanticModel = compilation.GetSemanticModel(tree); var root = tree.GetRoot(); - foreach(var nodes in root.DescendantNodes()) + // Conversions depend on declarations and their attributes, never executable bodies. + foreach (var nodes in root.DescendantNodes(static node => node is not + (BaseMethodDeclarationSyntax or BasePropertyDeclarationSyntax or BaseFieldDeclarationSyntax))) { cancellationToken.ThrowIfCancellationRequested(); - if(nodes is MethodDeclarationSyntax method) + if (nodes is MethodDeclarationSyntax method) { + if (method.AttributeLists.Count == 0) + { + continue; + } + var methodSymbol = semanticModel.GetDeclaredSymbol(method); if (methodSymbol == null) { continue; } - if (!IsTestMethod(methodSymbol)) + if (!IsTestMethod(methodSymbol, baseTestAttribute)) { continue; } @@ -148,7 +156,7 @@ private void ScanTestParameters(Compilation compilation, List co continue; } - if (!IsTestClass(classSymbol)) + if (!IsTestClass(classSymbol, baseTestAttribute)) { continue; } @@ -179,35 +187,42 @@ private void ScanTestParameters(Compilation compilation, List co } } - private static bool IsTestMethod(IMethodSymbol method) + private static bool IsTestMethod(IMethodSymbol method, INamedTypeSymbol? baseTestAttribute) { - return method.GetAttributes().Any(attr => + foreach (var attr in method.GetAttributes()) { var attrClass = attr.AttributeClass; if (attrClass == null) { - return false; + continue; } var baseType = attrClass; while (baseType != null) { - if (baseType.ToDisplayString() == WellKnownFullyQualifiedClassNames.BaseTestAttribute.WithoutGlobalPrefix) + if (SymbolEqualityComparer.Default.Equals(baseType, baseTestAttribute)) { return true; } baseType = baseType.BaseType; } - return false; - }); + } + + return false; } - private bool IsTestClass(INamedTypeSymbol classSymbol) + private static bool IsTestClass(INamedTypeSymbol classSymbol, INamedTypeSymbol? baseTestAttribute) { - return classSymbol.GetMembers() - .OfType() - .Any(IsTestMethod); + foreach (var member in classSymbol.GetMembers()) + { + if (member is IMethodSymbol method && IsTestMethod(method, baseTestAttribute)) + { + return true; + } + } + + return false; } private void ScanAttributesForTypes(ImmutableArray attributes, HashSet typesToScan) diff --git a/tests/TUnit.Core.SourceGenerator.Tests/AotConverterScanTests.cs b/tests/TUnit.Core.SourceGenerator.Tests/AotConverterScanTests.cs new file mode 100644 index 0000000000..ee54189a41 --- /dev/null +++ b/tests/TUnit.Core.SourceGenerator.Tests/AotConverterScanTests.cs @@ -0,0 +1,71 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TUnit.Core.SourceGenerator.Generators; + +namespace TUnit.Core.SourceGenerator.Tests; + +public class AotConverterScanTests +{ + [Test] + public async Task FindsConversionsInNestedAndPartialDeclarations() + { + const string source = """ + using TUnit.Core; + public struct MethodValue { public static implicit operator MethodValue(int value) => new(); } + public struct ConstructorValue { public static implicit operator ConstructorValue(int value) => new(); } + public struct NestedValue { public static implicit operator NestedValue(int value) => new(); } + public struct PartialValue { public static implicit operator PartialValue(int value) => new(); } + public struct BodyOnlyValue { public static implicit operator BodyOnlyValue(int value) => new(); } + public partial class Tests + { + [Test] + public partial void Partial(System.Type value); + + [Test, Arguments(1)] + public void Test(MethodValue value) + { + BodyOnlyValue local = 1; + void Helper() { BodyOnlyValue inner = 2; } + Helper(); + } + public class Nested + { + [Test, Arguments(1)] + public void Test(NestedValue value) { } + } + } + """; + const string secondPart = """ + using TUnit.Core; + [Arguments(1)] + public partial class Tests + { + public partial void Partial([Arguments(typeof(PartialValue))] System.Type value) { } + public Tests(ConstructorValue value) { BodyOnlyValue local = 1; } + public BodyOnlyValue Property => 1; + private BodyOnlyValue field = 1; + } + """; + + var parseOptions = new CSharpParseOptions(LanguageVersion.Preview); + var compilation = CSharpCompilation.Create( + "ConverterDeclarations", + [CSharpSyntaxTree.ParseText(source, parseOptions), CSharpSyntaxTree.ParseText(secondPart, parseOptions)], + ReferencesHelper.References, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create( + [new AotConverterGenerator().AsSourceGenerator()], parseOptions: parseOptions); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out var diagnostics); + + await Assert.That(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)).IsEmpty(); + await Assert.That(output.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error)).IsEmpty(); + + var generated = string.Join("\n", driver.GetRunResult().GeneratedTrees.Select(t => t.ToString())); + await Assert.That(generated).Contains("global::MethodValue"); + await Assert.That(generated).Contains("global::ConstructorValue"); + await Assert.That(generated).Contains("global::NestedValue"); + await Assert.That(generated).Contains("global::PartialValue"); + await Assert.That(generated).DoesNotContain("global::BodyOnlyValue"); + } +}