Repository navigation
Preserve tuple inference and runtime element conversions #7631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
59e8460
74a6771
ef301ff
2d62d58
b12f8fa
e7e1fc5
acd19d8
5688db4
f1d24d3
b4158b4
9d4b91d
0589abe
6c32140
04db53d
b2fec36
b8056fd
313e9a4
c18b6c0
2a1cc37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,7 @@ | |
| private const string InstanceMethodDataSourceAttributeName = "global::TUnit.Core.InstanceMethodDataSourceAttribute"; | ||
|
|
||
| private static readonly DiagnosticDescriptor GenerationErrorDescriptor = new( | ||
| GeneratorDiagnosticIds.TestMetadataGenerationFailed, | ||
|
Check warning on line 30 in src/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
|
||
| "Source Generation Error", | ||
| "Failed to generate test metadata for {0}.{1}: {2}", | ||
| "TUnit", | ||
|
|
@@ -7288,7 +7288,7 @@ | |
| return namedType.TupleElements.Select(element => element.Type).ToArray(); | ||
| } | ||
|
|
||
| if (namedType is not { Name: "Tuple", Arity: >= 1 and <= 8, ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } }) | ||
| if (namedType is not { Name: "Tuple" or "ValueTuple", Arity: >= 1 and <= 8, ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } }) | ||
| { | ||
| return null; | ||
| } | ||
|
|
@@ -7303,6 +7303,137 @@ | |
| : null; | ||
| } | ||
|
|
||
| private static bool MatchesOpenRowType(ITypeSymbol parameter, ITypeSymbol row, Compilation compilation, | ||
| Dictionary<ITypeSymbol, ITypeSymbol> bindings, bool reconstructTupleElements, bool overwriteBindings = false) | ||
| { | ||
| if (parameter is ITypeParameterSymbol) | ||
| { | ||
| // Structural tuple-element inference follows ProcessTypeForGenerics: later | ||
| // occurrences win. Interface/base candidates deliberately use strict bindings. | ||
| if (overwriteBindings) | ||
| { | ||
| bindings[parameter] = row; | ||
| return true; | ||
| } | ||
| if (bindings.TryGetValue(parameter, out var bound)) | ||
| return SymbolEqualityComparer.Default.Equals(bound, row); | ||
| bindings.Add(parameter, row); | ||
| return true; | ||
| } | ||
| // A sole tuple parameter is reconstructed element by element at invocation. | ||
| // Closed elements do not contribute to inference: their values may support runtime | ||
| // conversions that their declared types cannot prove (for example object boxing int). | ||
| // Whole rows and interface/base candidates retain exact closed-argument matching. | ||
| // overwriteBindings marks structural element inference, not candidate validation. | ||
| if (!CodeGenerationHelpers.ContainsTypeParameter(parameter)) | ||
| { | ||
| return (reconstructTupleElements && overwriteBindings) || SymbolEqualityComparer.Default.Equals(parameter, row); | ||
|
thomhurst marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| // Tuple reconstruction can lift a non-nullable value into an open nullable element, | ||
| // just as ProcessTypeForGenerics and reflection TryInferTypeMapping do during inference. | ||
| if (reconstructTupleElements | ||
| && parameter is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableParameter | ||
| && row.IsValueType | ||
| && row is not INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T }) | ||
| { | ||
| return MatchesOpenRowType(nullableParameter.TypeArguments[0], row, compilation, bindings, true, overwriteBindings); | ||
| } | ||
|
|
||
| // A single tuple argument is reconstructed by the generated binder, including across | ||
| // Tuple and ValueTuple families. Multi-parameter rows require an exact constructed shape, | ||
| // matching TupleHelper.MatchesOpenTuple, since their binder uses whole-type assignability. | ||
| if (reconstructTupleElements && GetTupleElementTypes(parameter) is { } parameterElements | ||
| && GetTupleElementTypes(row) is { } rowElements && parameterElements.Length == rowElements.Length) | ||
| { | ||
| for (var i = 0; i < parameterElements.Length; i++) | ||
| { | ||
| // Sole tuple arguments are rebuilt with per-element conversions. Preserve | ||
| // the existing last-element inference through structurally matching elements; | ||
| // nested interface/base candidates still require consistent bindings. | ||
| var parameterElement = parameterElements[i]; | ||
| var rowElement = rowElements[i]; | ||
| if (parameterElement is INamedTypeSymbol | ||
| { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableElement | ||
| && rowElement.IsValueType) | ||
| { | ||
| parameterElement = nullableElement.TypeArguments[0]; | ||
| if (rowElement is INamedTypeSymbol | ||
| { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableRowElement) | ||
| rowElement = nullableRowElement.TypeArguments[0]; | ||
| } | ||
| if (parameterElement is ITypeParameterSymbol) | ||
| { | ||
| if (!MatchesOpenRowType(parameterElement, rowElement, compilation, bindings, true, overwriteBindings)) return false; | ||
| continue; | ||
| } | ||
| if (!MatchesOpenRowType(parameterElements[i], rowElements[i], compilation, bindings, true, overwriteBindings)) return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| if (parameter is IArrayTypeSymbol array | ||
| && row is IArrayTypeSymbol rowArray && array.Rank == rowArray.Rank) | ||
| { | ||
| return MatchesOpenRowType(array.ElementType, rowArray.ElementType, compilation, bindings, reconstructTupleElements, overwriteBindings); | ||
| } | ||
|
|
||
| if (parameter is INamedTypeSymbol { IsGenericType: true } named | ||
| && row is INamedTypeSymbol { IsGenericType: true } rowNamed | ||
| && SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, rowNamed.OriginalDefinition)) | ||
| { | ||
| var validateCandidate = reconstructTupleElements && !overwriteBindings; | ||
| for (var i = 0; i < named.TypeArguments.Length; i++) | ||
| { | ||
| // Infer the open arguments first. Closed arguments are checked on the completed | ||
| // candidate below, where interface variance has its correct direction and scope. | ||
| if (validateCandidate && !CodeGenerationHelpers.ContainsTypeParameter(named.TypeArguments[i])) continue; | ||
| if (!MatchesOpenRowType(named.TypeArguments[i], rowNamed.TypeArguments[i], compilation, bindings, reconstructTupleElements, overwriteBindings)) return false; | ||
| } | ||
| if (validateCandidate) | ||
| { | ||
| var typeParameters = bindings.Keys.Cast<ITypeParameterSymbol>().ToImmutableArray(); | ||
| var typeArguments = typeParameters.Select(typeParameter => bindings[typeParameter]).ToArray(); | ||
| var completedParameter = SubstituteTypeParameters(parameter, typeParameters, typeArguments, compilation); | ||
| return SatisfiesTypeConstraint(row, completedParameter, compilation); | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| // The binder also supports converting a derived element to a generic base class. | ||
| // Only reconstruction permits this conversion; whole multi-parameter rows stay invariant. | ||
| if (reconstructTupleElements && parameter is INamedTypeSymbol { IsGenericType: true, TypeKind: TypeKind.Class } classParameter) | ||
| { | ||
| for (var baseType = row.BaseType; baseType is not null; baseType = baseType.BaseType) | ||
| { | ||
| if (!SymbolEqualityComparer.Default.Equals(classParameter.OriginalDefinition, baseType.OriginalDefinition)) continue; | ||
| var candidateBindings = new Dictionary<ITypeSymbol, ITypeSymbol>(bindings, SymbolEqualityComparer.Default); | ||
| if (!MatchesOpenRowType(parameter, baseType, compilation, candidateBindings, true)) continue; | ||
| foreach (var binding in candidateBindings) bindings[binding.Key] = binding.Value; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| // Reconstructed tuple elements may bind a concrete collection to an open interface. | ||
| // Keep candidate bindings isolated so a rejected interface cannot affect another match. | ||
| if (reconstructTupleElements && parameter is INamedTypeSymbol { IsGenericType: true, TypeKind: TypeKind.Interface } interfaceParameter) | ||
| { | ||
| foreach (var implementedInterface in row.AllInterfaces) | ||
| { | ||
| if (!SymbolEqualityComparer.Default.Equals(interfaceParameter.OriginalDefinition, implementedInterface.OriginalDefinition)) continue; | ||
| var candidateBindings = new Dictionary<ITypeSymbol, ITypeSymbol>(bindings, SymbolEqualityComparer.Default); | ||
| if (!MatchesOpenRowType(parameter, implementedInterface, compilation, candidateBindings, true)) continue; | ||
| foreach (var binding in candidateBindings) bindings[binding.Key] = binding.Value; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| // An unmatched reconstructed element supplies no structural bindings. Its declared | ||
| // interface/base type may hide a compatible runtime value; let the binder cast it. | ||
| // Candidate validation remains strict because it never overwrites bindings. | ||
| return reconstructTupleElements && overwriteBindings; | ||
|
thomhurst marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| private static ITypeSymbol[]? InferTypesFromMethodDataSourceRowType(TestMethodMetadata testMethod, AttributeData mdsAttr, ITypeSymbol[]? classTypes = null) | ||
| { | ||
| if (mdsAttr.ConstructorArguments.Length == 0) | ||
|
|
@@ -7346,14 +7477,38 @@ | |
| var genericParams = testMethod.MethodSymbol.TypeParameters; | ||
| var genericParamMap = new Dictionary<string, ITypeSymbol>(); | ||
|
|
||
| if (rowType is INamedTypeSymbol { IsTupleType: true } tupleType | ||
| && !(testMethodParams.Length == 1 && testMethodParams[0].Type.IsTupleType)) | ||
| // A typed tuple parameter receives the whole row when the remaining parameters can be | ||
| // omitted (optional or a trailing collection). A bare type parameter normally | ||
| // receives the first element, as reflection mode unwraps tuple rows before inference. | ||
| // With explicit method types, preserve whole reference-tuple inference so it deduplicates | ||
| // against GenerateGenericTest<Tuple<...>> instead of adding a first-element instantiation. | ||
| Dictionary<ITypeSymbol, ITypeSymbol>? wholeTupleBindings = null; | ||
| var receivesWholeTuple = testMethodParams.Length > 0 | ||
| && ((GetTupleElementTypes(CloseClassType(testMethodParams[0].Type)) is not null | ||
| && testMethodParams.Skip(1).All(parameter => parameter.IsOptional || parameter.IsParams | ||
| || SymbolEqualityComparer.Default.Equals(parameter, testMethodParams[^1]) && parameter.CollectsTrailingArguments()) | ||
| && MatchesOpenRowType(CloseClassType(testMethodParams[0].Type), rowType, compilation, | ||
| wholeTupleBindings = new Dictionary<ITypeSymbol, ITypeSymbol>(SymbolEqualityComparer.Default), | ||
|
thomhurst marked this conversation as resolved.
Resolved
|
||
| reconstructTupleElements: testMethodParams.Length == 1, overwriteBindings: testMethodParams.Length == 1)) | ||
|
thomhurst marked this conversation as resolved.
Resolved
|
||
| || (testMethodParams.Length == 1 | ||
| && testMethodParams[0].Type is ITypeParameterSymbol | ||
| && rowType is INamedTypeSymbol { IsTupleType: false } | ||
| && testMethod.MethodAttributes.Any(static attribute => attribute.AttributeClass?.IsOrInherits("global::TUnit.Core.GenerateGenericTestAttribute") is true))); | ||
|
|
||
| if (GetTupleElementTypes(rowType) is { } tupleElements && !receivesWholeTuple) | ||
| { | ||
| // A tuple row is spread across the test method's parameters | ||
| var tupleElements = tupleType.TupleElements; | ||
| for (var i = 0; i < testMethodParams.Length && i < tupleElements.Length; i++) | ||
| { | ||
| ProcessTypeForGenerics(CloseClassType(testMethodParams[i].Type), tupleElements[i].Type, genericParams, genericParamMap); | ||
| ProcessTypeForGenerics(CloseClassType(testMethodParams[i].Type), tupleElements[i], genericParams, genericParamMap); | ||
| } | ||
| } | ||
| else if (receivesWholeTuple && wholeTupleBindings is { Count: > 0 }) | ||
| { | ||
| foreach (var binding in wholeTupleBindings) | ||
| { | ||
| if (binding.Key is ITypeParameterSymbol parameter && genericParams.Contains(parameter)) | ||
| genericParamMap[parameter.Name] = binding.Value; | ||
| } | ||
| } | ||
| else if (testMethodParams.Length > 0) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.