Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c04cb43
fix(sourcegen): infer generic method types from any MethodDataSource …
thomhurst Oct 19, 2026
113caa7
fix(sourcegen): share MethodDataSource member resolution between fact…
thomhurst Oct 19, 2026
b6c4265
fix(sourcegen): call the chosen MethodDataSource overload the way ref…
thomhurst Oct 19, 2026
316efc9
test(sourcegen): cover an exact argument type beating an implicit con…
thomhurst Oct 19, 2026
9a53b50
fix(sourcegen): keep the type of MethodDataSource Arguments values
thomhurst Oct 19, 2026
2cce80f
fix: pick the MethodDataSource overload that accepts a null argument …
thomhurst Oct 20, 2026
fa94fe3
fix: handle field data sources, parameters without Arguments, and nul…
thomhurst Oct 20, 2026
1bf7dc9
fix: keep private field data sources on the reflection fallback, brea…
thomhurst Oct 20, 2026
b9a65ec
fix(sourcegen): keep a leading CancellationToken before a single tupl…
thomhurst Oct 20, 2026
90058d9
test(sourcegen): cover an overloaded data method selected without Arg…
thomhurst Oct 20, 2026
a105d29
fix(sourcegen): throw a descriptive error for a missing data source a…
thomhurst Oct 20, 2026
61230b6
perf(sourcegen): look up MethodDataSource row wrapper types once per …
thomhurst Oct 20, 2026
1945e14
fix(sourcegen): read private data source fields and properties in a t…
thomhurst Oct 20, 2026
6ba76de
fix(sourcegen): split a private object-typed data source into rows by…
thomhurst Oct 20, 2026
02a9e8c
fix(core): keep a null private data member as one row holding null
thomhurst Oct 20, 2026
13a95a0
test: cover a private data source member on a generic class
thomhurst Oct 20, 2026
aa09236
Merge remote-tracking branch 'origin/main' into fix/issue-7108-generi…
thomhurst Oct 20, 2026
d41ef61
Merge remote-tracking branch 'origin/main' into fix/issue-7108-generi…
thomhurst Oct 20, 2026
e4eade4
Merge remote-tracking branch 'origin/main' into fix/issue-7108-generi…
thomhurst Oct 20, 2026
cf851aa
fix(sourcegen): pick the MethodDataSource overload reflection mode ca…
thomhurst Oct 20, 2026
d265b7d
fix(sourcegen): split object-typed fields by runtime value; skip infe…
thomhurst Oct 20, 2026
81a9d78
fix: guard the overload specificity checks instead of relying on Debu…
thomhurst Oct 20, 2026
e2d678f
test: cover inherited protected data sources and a generic test with …
thomhurst Oct 20, 2026
c71d27e
test: read the instance object field through [MethodDataSource]; fix …
thomhurst Oct 20, 2026
890cfc8
Merge remote-tracking branch 'origin/main' into fix/issue-7108-generi…
thomhurst Oct 20, 2026
93db536
test(analyzers): pin the generic data method lookup in TUnit0001
thomhurst Oct 20, 2026
25c8c29
Merge origin/main; escape keyword-named data source members; fix net4…
thomhurst Oct 20, 2026
04dc119
test: drop the reflection-mode skip now that #7173 is fixed
thomhurst Oct 20, 2026
51792a4
fix(sourcegen): infer generic row types through only the delegate lay…
thomhurst Oct 20, 2026
d3035e5
Merge remote-tracking branch 'origin/main' into fix/issue-7108-generi…
thomhurst Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/TUnit.Analyzers/TestDataAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,19 @@ private void CheckMethodDataSource(SymbolAnalysisContext context,

// An overload that takes exactly the Arguments wins over one that also takes an injected trailing
// CancellationToken, as it does at runtime, whatever order the overloads are declared in.
// A generic data method is skipped while a non-generic one exists: neither mode can call it without
// type arguments, so IEnumerable<int> Rows() is used over IEnumerable<T> Rows<T>().
var dataSourceMethod = methodSymbols
.FirstOrDefault(methodSymbol =>
methodSymbol.Name == methodName &&
!methodSymbol.IsGenericMethod &&
ArgumentsMatchParameterTypes(context, argumentsForMethodCall, methodSymbol.Parameters.Select(p => p.Type)))
?? methodSymbols
.FirstOrDefault(methodSymbol =>
methodSymbol.Name == methodName &&
!methodSymbol.IsGenericMethod &&
ArgumentsMatchParameterTypes(context, argumentsForMethodCall, methodSymbol.Parameters.WithoutCancellationTokenParameter().Select(p => p.Type)))
?? methodSymbols.FirstOrDefault(x => x.Name == methodName && !x.IsGenericMethod)
?? methodSymbols.FirstOrDefault(x => x.Name == methodName);

// If no method found, check for properties or fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ private static void GenerateIndividualPropertyInitializer(CodeWriter writer, Pro
writer.AppendLine($"Type = typeof({propertyData.Property.GloballyQualifiedType}),");
writer.AppendLine($"IsStatic = true,");
writer.AppendLine($"ReflectionInfo = typeof({typeName}).GetProperty(\"{propertyName}\", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static),");
writer.AppendLine($"Getter = _ => {typeName}.{propertyName},");
writer.AppendLine($"Getter = _ => {typeName}.{IdentifierHelper.Escape(propertyName)},");
writer.AppendLine("ClassMetadata = containingTypeMetadata,");
writer.AppendLine("ContainingTypeMetadata = containingTypeMetadata");
writer.Unindent();
Expand Down Expand Up @@ -439,7 +439,7 @@ private static void GenerateIndividualPropertyInitializer(CodeWriter writer, Pro
writer.AppendLine("if (value != null)");
writer.AppendLine("{");
writer.Indent();
writer.AppendLine($"{typeName}.{propertyName} = ({propertyData.Property.GloballyQualifiedType})value;");
writer.AppendLine($"{typeName}.{IdentifierHelper.Escape(propertyName)} = ({propertyData.Property.GloballyQualifiedType})value;");
writer.Unindent();
writer.AppendLine("}");
writer.AppendLine();
Expand Down
771 changes: 625 additions & 146 deletions src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/TUnit.Core.SourceGenerator/Helpers/IdentifierHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.CodeAnalysis.CSharp;

namespace TUnit.Core.SourceGenerator.Helpers;

internal static class IdentifierHelper
{
/// <summary>
/// A symbol's Name drops the @ of an escaped identifier, so a member named after a keyword, such as
/// <c>@event</c>, needs it back before generated code can access the member.
/// </summary>
public static string Escape(string name)
=> SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None ? "@" + name : name;
}
24 changes: 23 additions & 1 deletion src/TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,29 @@

namespace TUnit.Core.SourceGenerator.Models;

public record CompilationContext(CSharpCompilation Compilation, AttributeWriter AttributeWriter, WellKnownTypes WellKnownTypes);
public record CompilationContext(CSharpCompilation Compilation, AttributeWriter AttributeWriter, WellKnownTypes WellKnownTypes)
{
private MethodDataSourceRowTypes? _methodDataSourceRowTypes;

/// <summary>
/// The generic definitions used to unwrap [MethodDataSource] row types, looked up once per compilation
/// on first use. A race only repeats the lookups: both instances hold the same symbols.
/// </summary>
public MethodDataSourceRowTypes MethodDataSourceRowTypes => _methodDataSourceRowTypes ??= new MethodDataSourceRowTypes(Compilation);
}

/// <summary>
/// The generic type definitions that wrap a [MethodDataSource] row. It holds symbols from one compilation, so,
/// like <see cref="CompilationContext"/>, it must never flow through the incremental pipeline.
/// </summary>
public sealed class MethodDataSourceRowTypes(Compilation compilation)
{
public INamedTypeSymbol? TaskType { get; } = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1");
public INamedTypeSymbol? ValueTaskType { get; } = compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1");
public INamedTypeSymbol? FuncType { get; } = compilation.GetTypeByMetadataName("System.Func`1");
public INamedTypeSymbol? AsyncEnumerableType { get; } = compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1");
public INamedTypeSymbol? TestDataRowType { get; } = compilation.GetTypeByMetadataName("TUnit.Core.TestDataRow`1");
}

/// <summary>
/// Contains all the metadata about a test method discovered by the source generator.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using TUnit.Core.SourceGenerator.Extensions;
using TUnit.Core.SourceGenerator.Helpers;

namespace TUnit.Core.SourceGenerator.Utilities;

Expand Down Expand Up @@ -331,13 +332,13 @@ private static string GetPropertyAccessor(INamedTypeSymbol namedTypeSymbol, IPro
// Use reflection to get the value at runtime
? $"_ => typeof({namedTypeSymbol.GloballyQualified()}).GetProperty(\"{property.Name}\")?.GetValue(null)"
// Use dynamic to avoid invalid cast to open generic type
: $"o => ((dynamic)o).{property.Name}";
: $"o => ((dynamic)o).{IdentifierHelper.Escape(property.Name)}";
}

var safeTypeName = namedTypeSymbol.GloballyQualified();
return property.IsStatic
? $"_ => {safeTypeName}.{property.Name}"
: $"o => (({safeTypeName})o).{property.Name}";
? $"_ => {safeTypeName}.{IdentifierHelper.Escape(property.Name)}"
: $"o => (({safeTypeName})o).{IdentifierHelper.Escape(property.Name)}";
}

/// <summary>
Expand Down
Loading
Loading