Skip to content

Commit 2b72467

Browse files
authored
feat: use IFormattable interface to format parameters culture-invariant (#617)
Updates the source generator’s parameter string formatting to be culture-invariant by using `IFormattable` with `InvariantCulture`, and adjusts a test helper + expectation to cover `double` formatting in setup strings. ### Key Changes: - Generate invariant-culture formatting for `IFormattable` parameters when emitting setup/value parameter string representations. - Update `IMyService` test helper signature to include a `double` parameter, and update `MockRegistryTests` expected `ToString()` output accordingly. - Also add the `OverloadResolutionAttribute` to indexers.
1 parent cf6ff26 commit 2b72467

8 files changed

Lines changed: 104 additions & 50 deletions

File tree

Source/Mockolate.SourceGenerators/Entities/Type.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ internal Type(ITypeSymbol typeSymbol)
3232
SpecialGenericType = typeSymbol.GetSpecialType();
3333
SpecialType = typeSymbol.SpecialType;
3434
CanBeNullable = typeSymbol.NullableAnnotation == NullableAnnotation.Annotated ||
35-
typeSymbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T };
35+
typeSymbol is INamedTypeSymbol
36+
{
37+
OriginalDefinition.SpecialType: SpecialType.System_Nullable_T,
38+
};
39+
IsFormattable = IsIFormattable(typeSymbol);
3640
}
3741

42+
public bool IsFormattable { get; }
3843
public bool CanBeNullable { get; }
3944
public SpecialType SpecialType { get; }
4045
public SpecialGenericType SpecialGenericType { get; }
@@ -45,5 +50,28 @@ internal Type(ITypeSymbol typeSymbol)
4550
internal static Type Void { get; } = new("void");
4651

4752
public string Fullname { get; }
53+
54+
private static bool IsIFormattable(ITypeSymbol typeSymbol)
55+
{
56+
// Unwrap Nullable<T>: check if the underlying type implements IFormattable
57+
if (typeSymbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, } nullable)
58+
{
59+
typeSymbol = nullable.TypeArguments[0];
60+
}
61+
62+
// The type itself may be System.IFormattable (AllInterfaces excludes self)
63+
if (typeSymbol is INamedTypeSymbol named &&
64+
named.ContainingNamespace?.ContainingNamespace?.IsGlobalNamespace == true &&
65+
named.ContainingNamespace.Name == "System" &&
66+
named.Name == "IFormattable")
67+
{
68+
return true;
69+
}
70+
71+
return typeSymbol.AllInterfaces.Any(i => i.ContainingNamespace?.ContainingNamespace?.IsGlobalNamespace == true &&
72+
i.ContainingNamespace.Name == "System" &&
73+
i.Name == "IFormattable");
74+
}
75+
4876
public override string ToString() => Fullname;
4977
}

Source/Mockolate.SourceGenerators/Helpers.cs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -319,19 +319,13 @@ public string ToNullableType()
319319
return parameter.Type.Fullname;
320320
}
321321

322-
public bool CanBeNullable()
323-
=> parameter.RefKind is not RefKind.Ref and not RefKind.Out and not RefKind.RefReadOnlyParameter &&
324-
parameter.Type.SpecialGenericType is not (SpecialGenericType.Span or SpecialGenericType.ReadOnlySpan);
325-
326322
/// <summary>
327-
/// Returns true if the parameter can be used as an explicit value in a T overload without creating
328-
/// ambiguity with the existing <c>IParameter&lt;T&gt;?</c> overload when <c>null</c> is passed.
329-
/// Nullable types (annotated reference types and nullable value types) are excluded because <c>null</c>
330-
/// would be ambiguous between <c>IParameter&lt;T&gt;?</c> and <c>T?</c>.
323+
/// Returns true if the parameter can be used as an explicit value in a T overload.
324+
/// Ambiguity with the <c>IParameter&lt;T&gt;?</c> overload when <c>null</c> is passed is
325+
/// resolved via the OverloadResolutionPriorityAttribute.
331326
/// </summary>
332-
public bool CanBeExplicitValue()
333-
=> parameter.CanBeNullable() &&
334-
!parameter.IsNullableAnnotated &&
335-
!parameter.Type.Fullname.EndsWith("?");
327+
public bool CanUseNullableParameterOverload()
328+
=> parameter.RefKind is not RefKind.Ref and not RefKind.Out and not RefKind.RefReadOnlyParameter &&
329+
parameter.Type.SpecialGenericType is not (SpecialGenericType.Span or SpecialGenericType.ReadOnlySpan);
336330
}
337331
}

Source/Mockolate.SourceGenerators/MockGeneratorHelpers.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ internal static IEnumerable<MockClass> ExtractMockOrMockFactoryCreateSyntaxOrDef
5252
this SyntaxNode syntaxNode, SemanticModel semanticModel)
5353
{
5454
IAssemblySymbol sourceAssembly = semanticModel.Compilation.Assembly;
55-
5655
// Static extension methods
5756
if (syntaxNode is InvocationExpressionSyntax invocation)
5857
{

Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,7 +1685,7 @@ private static IEnumerable<bool[]> GenerateValueFlagCombinations(EquatableArray<
16851685
{
16861686
int[] valueableIndices = parameters
16871687
.Select((p, i) => (p, i))
1688-
.Where(x => x.p.CanBeExplicitValue())
1688+
.Where(x => x.p.CanUseNullableParameterOverload())
16891689
.Select(x => x.i)
16901690
.ToArray();
16911691
int valueableCount = valueableIndices.Length;
@@ -1754,20 +1754,20 @@ private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberT
17541754
indexer.MemberType == memberType;
17551755
foreach (Property indexer in @class.AllProperties().Where(indexerPredicate))
17561756
{
1757-
AppendIndexerSetupDefinition(sb, indexer);
1757+
AppendIndexerSetupDefinition(sb, indexer, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
17581758
if (indexer.IndexerParameters!.Value.Count <= MaxExplicitParameters)
17591759
{
17601760
foreach (bool[] valueFlags in GenerateValueFlagCombinations(indexer.IndexerParameters.Value))
17611761
{
1762-
AppendIndexerSetupDefinition(sb, indexer, valueFlags);
1762+
AppendIndexerSetupDefinition(sb, indexer, valueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
17631763
}
17641764
}
17651765
else
17661766
{
1767-
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanBeExplicitValue()).ToArray();
1767+
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanUseNullableParameterOverload()).ToArray();
17681768
if (allValueFlags.Any(f => f))
17691769
{
1770-
AppendIndexerSetupDefinition(sb, indexer, allValueFlags);
1770+
AppendIndexerSetupDefinition(sb, indexer, allValueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
17711771
}
17721772
}
17731773
}
@@ -1809,7 +1809,7 @@ private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberT
18091809
}
18101810
else
18111811
{
1812-
bool[] allValueFlags = method.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
1812+
bool[] allValueFlags = method.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
18131813
if (allValueFlags.Any(f => f))
18141814
{
18151815
AppendMethodSetupDefinition(sb, @class, method, false, valueFlags: allValueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
@@ -1941,7 +1941,7 @@ private static void AppendMethodSetupDefinition(StringBuilder sb, Class @class,
19411941
else
19421942
{
19431943
sb.Append(parameter.ToParameter());
1944-
if (parameter.CanBeNullable())
1944+
if (parameter.CanUseNullableParameterOverload())
19451945
{
19461946
sb.Append('?');
19471947
}
@@ -2016,7 +2016,7 @@ private static void ImplementSetupInterface(StringBuilder sb, Class @class, stri
20162016
}
20172017
else
20182018
{
2019-
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanBeExplicitValue()).ToArray();
2019+
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanUseNullableParameterOverload()).ToArray();
20202020
if (allValueFlags.Any(f => f))
20212021
{
20222022
AppendIndexerSetupImplementation(sb, indexer, mockRegistryName, setupName, allValueFlags);
@@ -2061,7 +2061,7 @@ private static void ImplementSetupInterface(StringBuilder sb, Class @class, stri
20612061
}
20622062
else
20632063
{
2064-
bool[] allValueFlags = method.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
2064+
bool[] allValueFlags = method.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
20652065
if (allValueFlags.Any(f => f))
20662066
{
20672067
AppendMethodSetupImplementation(sb, method, mockRegistryName, setupName, false, valueFlags: allValueFlags);
@@ -2140,7 +2140,7 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met
21402140
else
21412141
{
21422142
sb.Append(parameter.ToParameter());
2143-
if (parameter.CanBeNullable())
2143+
if (parameter.CanUseNullableParameterOverload())
21442144
{
21452145
sb.Append('?');
21462146
}
@@ -2225,10 +2225,15 @@ private static void AppendMethodSetupImplementation(StringBuilder sb, Method met
22252225
sb.AppendLine();
22262226
}
22272227

2228-
private static void AppendIndexerSetupDefinition(StringBuilder sb, Property indexer, bool[]? valueFlags = null)
2228+
private static void AppendIndexerSetupDefinition(StringBuilder sb, Property indexer, bool[]? valueFlags = null, bool hasOverloadResolutionPriority = false)
22292229
{
22302230
sb.AppendXmlSummary(
22312231
$"Setup for the {indexer.Type.Fullname.EscapeForXmlDoc()} indexer <see cref=\"{indexer.ContainingType.EscapeForXmlDoc()}.this[{string.Join(", ", indexer.IndexerParameters!.Value.Select(p => p.RefKind.GetString() + p.Type.Fullname.EscapeForXmlDoc()))}]\" />");
2232+
if (hasOverloadResolutionPriority)
2233+
{
2234+
sb.Append("\t\t[global::System.Runtime.CompilerServices.OverloadResolutionPriority(").Append(valueFlags?.Count(x => !x).ToString() ?? "int.MaxValue").Append(")]").AppendLine();
2235+
}
2236+
22322237
sb.Append("\t\tglobal::Mockolate.Setup.IndexerSetup<").AppendTypeOrWrapper(indexer.Type);
22332238
foreach (MethodParameter parameter in indexer.IndexerParameters!)
22342239
{
@@ -2252,7 +2257,7 @@ private static void AppendIndexerSetupDefinition(StringBuilder sb, Property inde
22522257
else
22532258
{
22542259
sb.Append(parameter.ToParameter());
2255-
if (parameter.CanBeNullable())
2260+
if (parameter.CanUseNullableParameterOverload())
22562261
{
22572262
sb.Append('?');
22582263
}
@@ -2294,7 +2299,7 @@ private static void AppendIndexerSetupImplementation(StringBuilder sb, Property
22942299
else
22952300
{
22962301
sb.Append(parameter.ToParameter());
2297-
if (parameter.CanBeNullable())
2302+
if (parameter.CanUseNullableParameterOverload())
22982303
{
22992304
sb.Append('?');
23002305
}
@@ -2333,7 +2338,7 @@ private static void AppendIndexerSetupImplementation(StringBuilder sb, Property
23332338
else
23342339
{
23352340
sb.Append($"new global::Mockolate.Parameters.NamedParameter(\"{parameter.Name}\", (global::Mockolate.Parameters.IParameter)({paramRef}");
2336-
if (parameter.CanBeNullable())
2341+
if (parameter.CanUseNullableParameterOverload())
23372342
{
23382343
sb.Append($" ?? global::Mockolate.It.IsNull<{parameter.ToNullableType()}>(\"null\")");
23392344
}
@@ -2352,10 +2357,15 @@ private static void AppendIndexerSetupImplementation(StringBuilder sb, Property
23522357
sb.AppendLine();
23532358
}
23542359

2355-
private static void AppendIndexerVerifyDefinition(StringBuilder sb, Property indexer, string verifyName, bool[]? valueFlags = null)
2360+
private static void AppendIndexerVerifyDefinition(StringBuilder sb, Property indexer, string verifyName, bool[]? valueFlags = null, bool hasOverloadResolutionPriority = false)
23562361
{
23572362
sb.AppendXmlSummary(
23582363
$"Verify interactions with the {indexer.Type.Fullname.EscapeForXmlDoc()} indexer <see cref=\"{indexer.ContainingType.EscapeForXmlDoc()}.this[{string.Join(", ", indexer.IndexerParameters!.Value.Select(p => p.RefKind.GetString() + p.Type.Fullname.EscapeForXmlDoc()))}]\" />.");
2364+
if (hasOverloadResolutionPriority)
2365+
{
2366+
sb.Append("\t\t[global::System.Runtime.CompilerServices.OverloadResolutionPriority(").Append(valueFlags?.Count(x => !x).ToString() ?? "int.MaxValue").Append(")]").AppendLine();
2367+
}
2368+
23592369
sb.Append("\t\tglobal::Mockolate.Verify.VerificationIndexerResult<").Append(verifyName).Append(", ").AppendTypeOrWrapper(indexer.Type).Append("> this[");
23602370
int i = 0;
23612371
foreach (MethodParameter parameter in indexer.IndexerParameters!.Value)
@@ -2373,7 +2383,7 @@ private static void AppendIndexerVerifyDefinition(StringBuilder sb, Property ind
23732383
else
23742384
{
23752385
sb.Append(parameter.ToParameter());
2376-
if (parameter.CanBeNullable())
2386+
if (parameter.CanUseNullableParameterOverload())
23772387
{
23782388
sb.Append('?');
23792389
}
@@ -2409,7 +2419,7 @@ private static void AppendIndexerVerifyImplementation(StringBuilder sb, Property
24092419
else
24102420
{
24112421
sb.Append(parameter.ToParameter());
2412-
if (parameter.CanBeNullable())
2422+
if (parameter.CanUseNullableParameterOverload())
24132423
{
24142424
sb.Append('?');
24152425
}
@@ -2551,20 +2561,20 @@ private static void DefineVerifyInterface(StringBuilder sb, Class @class, string
25512561
indexer.MemberType == memberType;
25522562
foreach (Property indexer in @class.AllProperties().Where(indexerPredicate))
25532563
{
2554-
AppendIndexerVerifyDefinition(sb, indexer, verifyName);
2564+
AppendIndexerVerifyDefinition(sb, indexer, verifyName, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
25552565
if (indexer.IndexerParameters!.Value.Count <= MaxExplicitParameters)
25562566
{
25572567
foreach (bool[] valueFlags in GenerateValueFlagCombinations(indexer.IndexerParameters.Value))
25582568
{
2559-
AppendIndexerVerifyDefinition(sb, indexer, verifyName, valueFlags);
2569+
AppendIndexerVerifyDefinition(sb, indexer, verifyName, valueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
25602570
}
25612571
}
25622572
else
25632573
{
2564-
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanBeExplicitValue()).ToArray();
2574+
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanUseNullableParameterOverload()).ToArray();
25652575
if (allValueFlags.Any(f => f))
25662576
{
2567-
AppendIndexerVerifyDefinition(sb, indexer, verifyName, allValueFlags);
2577+
AppendIndexerVerifyDefinition(sb, indexer, verifyName, allValueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
25682578
}
25692579
}
25702580
}
@@ -2606,7 +2616,7 @@ private static void DefineVerifyInterface(StringBuilder sb, Class @class, string
26062616
}
26072617
else
26082618
{
2609-
bool[] allValueFlags = method.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
2619+
bool[] allValueFlags = method.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
26102620
if (allValueFlags.Any(f => f))
26112621
{
26122622
AppendMethodVerifyDefinition(sb, method, verifyName, false, valueFlags: allValueFlags, hasOverloadResolutionPriority: hasOverloadResolutionPriority);
@@ -2693,7 +2703,7 @@ private static void AppendMethodVerifyDefinition(StringBuilder sb, Method method
26932703
else
26942704
{
26952705
sb.AppendVerifyParameter(parameter);
2696-
if (parameter.CanBeNullable())
2706+
if (parameter.CanUseNullableParameterOverload())
26972707
{
26982708
sb.Append('?');
26992709
}
@@ -2760,7 +2770,7 @@ private static void ImplementVerifyInterface(StringBuilder sb, Class @class, str
27602770
}
27612771
else
27622772
{
2763-
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanBeExplicitValue()).ToArray();
2773+
bool[] allValueFlags = indexer.IndexerParameters.Value.Select(p => p.CanUseNullableParameterOverload()).ToArray();
27642774
if (allValueFlags.Any(f => f))
27652775
{
27662776
AppendIndexerVerifyImplementation(sb, indexer, mockRegistryName, verifyName, allValueFlags);
@@ -2804,7 +2814,7 @@ private static void ImplementVerifyInterface(StringBuilder sb, Class @class, str
28042814
}
28052815
else
28062816
{
2807-
bool[] allValueFlags = method.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
2817+
bool[] allValueFlags = method.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
28082818
if (allValueFlags.Any(f => f))
28092819
{
28102820
AppendMethodVerifyImplementation(sb, method, mockRegistryName, verifyName, false, valueFlags: allValueFlags);
@@ -2868,7 +2878,7 @@ private static void AppendMethodVerifyImplementation(StringBuilder sb, Method me
28682878
else
28692879
{
28702880
sb.AppendVerifyParameter(parameter);
2871-
if (parameter.CanBeNullable())
2881+
if (parameter.CanUseNullableParameterOverload())
28722882
{
28732883
sb.Append('?');
28742884
}

Source/Mockolate.SourceGenerators/Sources/Sources.MockDelegate.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public static string MockDelegate(string name, MockClass @class, Method delegate
188188
}
189189
else if (delegateMethod.Parameters.Count > MaxExplicitParameters)
190190
{
191-
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
191+
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
192192
if (allValueFlags.Any(f => f))
193193
{
194194
AppendMethodSetupImplementation(sb, delegateMethod, mockRegistryName, $"IMockSetupFor{name}", false, "Setup", allValueFlags);
@@ -210,7 +210,7 @@ public static string MockDelegate(string name, MockClass @class, Method delegate
210210
}
211211
else if (delegateMethod.Parameters.Count > MaxExplicitParameters)
212212
{
213-
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
213+
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
214214
if (allValueFlags.Any(f => f))
215215
{
216216
AppendMethodVerifyImplementation(sb, delegateMethod, mockRegistryName, $"IMockVerifyFor{name}", false, "Verify", allValueFlags);
@@ -265,7 +265,7 @@ public static string MockDelegate(string name, MockClass @class, Method delegate
265265
}
266266
else if (delegateMethod.Parameters.Count > MaxExplicitParameters)
267267
{
268-
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
268+
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
269269
if (allValueFlags.Any(f => f))
270270
{
271271
AppendMethodVerifyImplementation(sb, delegateMethod, mockRegistryName, $"IMockVerifyFor{name}", false, "Verify", allValueFlags);
@@ -325,7 +325,7 @@ public static string MockDelegate(string name, MockClass @class, Method delegate
325325
}
326326
else if (delegateMethod.Parameters.Count > MaxExplicitParameters)
327327
{
328-
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
328+
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
329329
if (allValueFlags.Any(f => f))
330330
{
331331
AppendMethodSetupDefinition(sb, @class, delegateMethod, false, "Setup", allValueFlags);
@@ -358,7 +358,7 @@ public static string MockDelegate(string name, MockClass @class, Method delegate
358358
}
359359
else if (delegateMethod.Parameters.Count > MaxExplicitParameters)
360360
{
361-
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanBeExplicitValue()).ToArray();
361+
bool[] allValueFlags = delegateMethod.Parameters.Select(p => p.CanUseNullableParameterOverload()).ToArray();
362362
if (allValueFlags.Any(f => f))
363363
{
364364
AppendMethodVerifyDefinition(sb, delegateMethod, $"IMockVerifyFor{name}", false, "Verify", allValueFlags);

Source/Mockolate.SourceGenerators/Sources/Sources.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private static void AppendNamedParameter(StringBuilder sb, MethodParameter param
107107
sb.Append("new global::Mockolate.Parameters.NamedParameter(\"").Append(parameter.Name)
108108
.Append("\", (global::Mockolate.Parameters.IParameter)(");
109109
sb.Append(parameter.Name);
110-
if (parameter.CanBeNullable())
110+
if (parameter.CanUseNullableParameterOverload())
111111
{
112112
if (parameter.HasExplicitDefaultValue)
113113
{
@@ -144,7 +144,16 @@ private static void AppendNamedValueParameter(StringBuilder sb, MethodParameter
144144
}
145145
else if (parameter.Type.CanBeNullable)
146146
{
147-
sb.Append(paramRef).Append("?.ToString() ?? \"null\"");
147+
sb.Append(paramRef).Append(" is null ? \"null\" : ");
148+
if (parameter.Type.IsFormattable)
149+
sb.Append("((global::System.IFormattable)").Append(paramRef).Append(").ToString(null, global::System.Globalization.CultureInfo.InvariantCulture)");
150+
else
151+
sb.Append(paramRef).Append(".ToString()");
152+
}
153+
else if (parameter.Type.IsFormattable)
154+
{
155+
sb.Append("((global::System.IFormattable)").Append(paramRef)
156+
.Append(").ToString(null, global::System.Globalization.CultureInfo.InvariantCulture)");
148157
}
149158
else
150159
{

0 commit comments

Comments
 (0)