Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 1 deletion src/TUnit.Core.SourceGenerator/CodeGenerationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ public static string GenerateTypeInfo(ITypeSymbol typeSymbol)
/// </summary>
private static string GetTypeOfExpression(ITypeSymbol typeSymbol)
{
var fullyQualifiedName = typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedGenericWithGlobalPrefix);
// typeof(dynamic) does not compile (CS1962); dynamic is object at runtime.
var fullyQualifiedName = typeSymbol.TypeKind == TypeKind.Dynamic
? "object"
: typeSymbol.ToDisplayString(DisplayFormats.FullyQualifiedGenericWithGlobalPrefix);
return $"typeof({fullyQualifiedName})";
}

Expand Down
8 changes: 8 additions & 0 deletions src/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ public static bool IsIEnumerable(this ITypeSymbol namedTypeSymbol, Compilation c

public static string GloballyQualified(this ISymbol typeSymbol)
{
// `dynamic` is `object` at runtime, and `typeof(dynamic)` does not compile (CS1962). Rendering it as `object`
// also binds generated calls statically instead of through the runtime binder. Nested uses such as
// `List<dynamic>` or `dynamic[]` are valid everywhere, including inside typeof, so only the top level changes.
if (typeSymbol is ITypeSymbol { TypeKind: TypeKind.Dynamic })
{
return "object";
}

// Handle open generic types where type arguments are type parameters
// This prevents invalid C# like List<T>, Dictionary<TKey, TValue>, T? where type parameters are undefined
if (typeSymbol is INamedTypeSymbol { IsGenericType: true, Arity: > 0 } namedTypeSymbol)
Expand Down
25 changes: 25 additions & 0 deletions tests/TUnit.Core.SourceGenerator.Tests/Bugs/7165/Tests7165.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace TUnit.Core.SourceGenerator.Tests;

/// <summary>
/// Snapshot coverage for https://github.com/thomhurst/TUnit/issues/7165: top-level <c>dynamic</c> parameters,
/// constructor parameters and properties are rendered as <c>object</c>, while nested uses such as
/// <c>List&lt;dynamic&gt;</c> and <c>dynamic[]</c> keep their spelling. <see cref="DynamicParameterCompilationTests"/>
/// checks that the generated code compiles.
/// </summary>
internal class Tests7165 : TestsBase
{
[Test]
public Task Test() => RunTest(Path.Combine(
Git.TestsDirectory.FullName,
"TUnit.TestProject",
"Bugs",
"_7165",
"DynamicParameterTests.cs"),
async generatedFiles =>
{
var source = string.Join(Environment.NewLine, generatedFiles);

await Assert.That(source).DoesNotContain("typeof(dynamic)");
await Assert.That(source).Contains("typeof(object)");
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Core.SourceGenerator.CodeGenerators;
using TUnit.Core.SourceGenerator.Generators;

namespace TUnit.Core.SourceGenerator.Tests;

/// <summary>
/// Regression test for https://github.com/thomhurst/TUnit/issues/7165.
///
/// <c>dynamic</c> test parameters, constructor parameters and data-source properties used to be rendered as <c>typeof(dynamic)</c>
/// in the generated metadata, which does not compile (CS1962). <c>dynamic</c> is <c>object</c> at runtime, so
/// the generator must render it as <c>typeof(object)</c>. The snapshot harness only checks generator
/// diagnostics, so this test compiles the generated output and asserts there are no compiler errors.
/// </summary>
internal class DynamicParameterCompilationTests
{
private const string Source =
"""
global using global::System;
global using global::System.Collections.Generic;
global using global::System.Threading.Tasks;
global using global::TUnit.Core;

namespace MyTests;

public class DynamicTests
{
[Test]
[Arguments("x")]
public void Takes_Dynamic(dynamic value)
{
}

[Test]
[Arguments(1, "a", "b")]
public void Takes_Dynamic_Params(int first, params dynamic[] values)
{
}

[Test]
[MethodDataSource(nameof(Data))]
public void Takes_Dynamic_Generic(List<dynamic> values, dynamic[] array)
{
}

public static IEnumerable<Func<(List<dynamic>, dynamic[])>> Data()
{
yield return () => (new List<dynamic> { 1 }, new dynamic[] { "a" });
}
}

[Arguments("x")]
public class DynamicConstructorTests(dynamic value)
{
[MethodDataSource(nameof(PropertyData))]
public dynamic Property { get; set; } = null!;

[MethodDataSource(nameof(PropertyData))]
public static dynamic StaticProperty { get; set; } = null!;

public static object PropertyData() => "y";

[Test]
public void Test()
{
}
}
""";

[Test]
public async Task Generated_metadata_for_dynamic_parameters_compiles()
{
// Generated code (and the project) targets the preview language version.
var parseOptions = new CSharpParseOptions(LanguageVersion.Preview);

var compilation = CSharpCompilation.Create(
"DynamicParameterRegression",
[CSharpSyntaxTree.ParseText(Source, parseOptions)],
ReferencesHelper.References,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

GeneratorDriver driver = CSharpGeneratorDriver.Create(
[new TestMetadataGenerator().AsSourceGenerator(), new PropertyInjectionSourceGenerator().AsSourceGenerator(), new StaticPropertyInitializationGenerator().AsSourceGenerator()],
parseOptions: parseOptions);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out var generatorDiagnostics);

// A generator that throws reports a "failed to generate source" warning and emits nothing, and the input
// alone still compiles. Fail on generator diagnostics and require every generator to produce output, so
// the compilation check below cannot pass vacuously.
var generatorFailures = generatorDiagnostics
.Where(d => d.Severity == DiagnosticSeverity.Error
|| (d.Severity == DiagnosticSeverity.Warning && d.GetMessage().Contains("failed to generate source")))
.ToArray();

await Assert.That(generatorFailures)
.IsEmpty()
.Because(string.Join(Environment.NewLine, generatorFailures.Select(d => d.ToString())));

foreach (var result in driver.GetRunResult().Results)
{
var generatorName = result.Generator.GetGeneratorType().Name;
await Assert.That(result.Exception).IsNull().Because($"{generatorName} threw");
await Assert.That(result.GeneratedSources).IsNotEmpty().Because($"{generatorName} must emit source for the dynamic members");
}

var errors = output.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.ToArray();

// Surface the full generated source if anything fails, so failures are diagnosable.
var generated = string.Join(
Environment.NewLine + "----" + Environment.NewLine,
output.SyntaxTrees.Select(t => t.GetText().ToString()));

await Assert.That(errors)
.IsEmpty()
Comment thread
thomhurst marked this conversation as resolved.
Resolved
.Because($"generated code for dynamic parameters must compile. Errors:{Environment.NewLine}"
+ string.Join(Environment.NewLine, errors.Select(e => e.ToString()))
+ Environment.NewLine + generated);
}
}
Loading
Loading