Skip to content

Commit 0f6d82d

Browse files
committed
Fixing translation of nested, multiple generic argument func types
1 parent ecbbbc3 commit 0f6d82d

File tree

2 files changed

+92
-77
lines changed

2 files changed

+92
-77
lines changed

ReadableExpressions.UnitTests/WhenTranslatingConstants.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,18 @@ public void ShouldTranslateAFuncWithNestedGenericParameters()
352352
Assert.AreEqual("Func<int?, FileInfo, Dictionary<IDictionary<FileInfo, string[]>, string>>", translated);
353353
}
354354

355+
[TestMethod]
356+
public void ShouldTranslateAnActionWithMultipleNestedGenericParameters()
357+
{
358+
Action<Generic<GenericOne<int>, GenericTwo<long>, GenericTwo<long>>> genericAction = fileInfo => { };
359+
360+
var actionConstant = Expression.Constant(genericAction);
361+
362+
var translated = actionConstant.ToReadableString();
363+
364+
Assert.AreEqual("Action<Generic<GenericOne<int>, GenericTwo<long>, GenericTwo<long>>>", translated);
365+
}
366+
355367
// See https://github.com/agileobjects/ReadableExpressions/issues/5
356368
[TestMethod]
357369
public void ShouldTranslateDbNullValue()
@@ -377,6 +389,12 @@ public void ShouldTranslateAnObjectConstant()
377389
}
378390
}
379391

392+
internal class GenericOne<T> { }
393+
394+
internal class GenericTwo<T> { }
395+
396+
internal class Generic<T1, T2, T3> { }
397+
380398
internal enum OddNumber
381399
{
382400
One = 1

ReadableExpressions/Translators/ConstantExpressionTranslator.cs

Lines changed: 74 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
namespace AgileObjects.ReadableExpressions.Translators
22
{
33
using System;
4-
using System.Collections.Generic;
54
using System.Globalization;
65
using System.Linq.Expressions;
76
#if NET_STANDARD
@@ -32,9 +31,7 @@ public override string Translate(Expression expression, TranslationContext conte
3231
return constant.Type.GetFriendlyName() + "." + constant.Value;
3332
}
3433

35-
string translation;
36-
37-
if (TryTranslateByTypeCode(constant, out translation))
34+
if (TryTranslateByTypeCode(constant, out var translation))
3835
{
3936
return translation;
4037
}
@@ -214,17 +211,17 @@ private static bool TryGetFactoryMethodCall(
214211

215212
private static string FormatNumeric(decimal value)
216213
{
217-
return (value % 1).Equals(0) ? value.ToString("0") : value.ToString();
214+
return (value % 1).Equals(0) ? value.ToString("0") : value.ToString(CultureInfo.CurrentCulture);
218215
}
219216

220217
private static string FormatNumeric(double value)
221218
{
222-
return (value % 1).Equals(0) ? value.ToString("0") : value.ToString();
219+
return (value % 1).Equals(0) ? value.ToString("0") : value.ToString(CultureInfo.CurrentCulture);
223220
}
224221

225222
private static string FormatNumeric(float value)
226223
{
227-
return (value % 1).Equals(0) ? value.ToString("0") : value.ToString();
224+
return (value % 1).Equals(0) ? value.ToString("0") : value.ToString(CultureInfo.CurrentCulture);
228225
}
229226

230227
private static bool IsType(ConstantExpression constant, out string translation)
@@ -239,116 +236,116 @@ private static bool IsType(ConstantExpression constant, out string translation)
239236
return false;
240237
}
241238

242-
private static readonly Regex _funcMatcher = new Regex(@"^System\.(?<Type>Func|Action)`\d+\[(?<Arguments>.+)\]$");
239+
private static readonly Regex _funcMatcher = new Regex(@"^System\.(?:Func|Action)`\d+\[.+\]$");
243240

244241
private static bool IsFunc(ConstantExpression constant, out string translation)
245242
{
246243
var match = _funcMatcher.Match(constant.Value.ToString());
247244

248-
if (!match.Success)
245+
if (match.Success)
249246
{
250-
translation = null;
251-
return false;
247+
translation = ParseFuncFrom(match.Value);
248+
return true;
252249
}
253250

254-
var funcType = match.Groups["Type"].Value;
255-
var argumentTypes = ParseArgumentNames(match.Groups["Arguments"].Value.Split(','));
256-
257-
translation = GetGenericTypeName(funcType, argumentTypes);
258-
return true;
251+
translation = null;
252+
return false;
259253
}
260254

261255
#region IsFunc Helpers
262256

263-
private static IEnumerable<string> ParseArgumentNames(params string[] arguments)
257+
private static string ParseFuncFrom(string funcString)
264258
{
265-
for (var i = 0; i < arguments.Length; i++)
259+
var symbols = new[] { '`', ',', '[', ']' };
260+
var parseIndex = 0;
261+
var parsedFunc = string.Empty;
262+
263+
while (true)
266264
{
267-
var argument = arguments[i];
268-
var genericArgumentsIndex = argument.IndexOf('`');
265+
var nextSymbolIndex = funcString.IndexOfAny(symbols, parseIndex);
269266

270-
if (genericArgumentsIndex == -1)
267+
if (nextSymbolIndex == -1)
271268
{
272-
yield return GetTypeName(argument);
273-
continue;
274-
}
269+
var substring = funcString.Substring(parseIndex);
275270

276-
if (argument.StartsWith("System.Nullable`"))
277-
{
278-
yield return GetNullableTypeName(argument);
279-
continue;
271+
if (substring.Length > 0)
272+
{
273+
var typeName = GetTypeName(substring);
274+
275+
parsedFunc += typeName;
276+
}
277+
break;
280278
}
281279

282-
var genericTypeName = GetTypeName(argument.Substring(0, genericArgumentsIndex));
283-
var genericTypeArgumentsStart = argument.IndexOf('[', genericArgumentsIndex) + 1;
284-
argument = argument.Substring(genericTypeArgumentsStart);
285-
var genericTypeArgumentsClosesNeeded = 1;
286-
var genericTypeArgumentsString = argument;
280+
var substringLength = nextSymbolIndex - parseIndex;
287281

288-
if (i == arguments.Length - 1)
282+
if (substringLength > 0)
289283
{
290-
genericTypeArgumentsString = RemoveFinalGenericArgumentsClose(genericTypeArgumentsString);
284+
var substring = funcString.Substring(parseIndex, substringLength);
285+
286+
if (substring == "System.Nullable")
287+
{
288+
var nullableTypeIndex = parseIndex + "System.Nullable`1[".Length;
289+
var nullableTypeEndIndex = funcString.IndexOf(']', nullableTypeIndex);
290+
var nullableTypeLength = nullableTypeEndIndex - nullableTypeIndex;
291+
substring = funcString.Substring(nullableTypeIndex, nullableTypeLength);
292+
293+
var typeName = GetTypeName(substring);
294+
295+
parsedFunc += typeName + "?";
296+
parseIndex = nullableTypeEndIndex + 1;
297+
continue;
298+
}
299+
else
300+
{
301+
var typeName = GetTypeName(substring);
302+
303+
parsedFunc += typeName;
304+
parseIndex = nextSymbolIndex + 1;
305+
}
291306
}
292-
else
307+
308+
switch (funcString[nextSymbolIndex])
293309
{
294-
while (i < arguments.Length - 1)
295-
{
296-
if (argument.IndexOf('`') != -1)
297-
{
298-
++genericTypeArgumentsClosesNeeded;
299-
}
310+
case '`':
311+
parsedFunc += "<";
312+
parseIndex = funcString.IndexOf('[', parseIndex) + 1;
313+
continue;
300314

301-
argument = arguments[++i];
302-
genericTypeArgumentsString += "," + argument;
315+
case ',':
316+
parsedFunc += ", ";
303317

304-
if (argument.EndsWith(']') && (argument[argument.Length - 2] != '['))
318+
if (substringLength == 0)
305319
{
306-
--genericTypeArgumentsClosesNeeded;
320+
++parseIndex;
307321
}
322+
continue;
323+
324+
case '[':
325+
parsedFunc += "[]";
326+
++parseIndex;
327+
continue;
328+
329+
case ']':
330+
parsedFunc += ">";
308331

309-
if (genericTypeArgumentsClosesNeeded == 0)
332+
if (substringLength == 0)
310333
{
311-
genericTypeArgumentsString = RemoveFinalGenericArgumentsClose(genericTypeArgumentsString);
312-
break;
334+
++parseIndex;
313335
}
314-
}
336+
break;
315337
}
316-
317-
var genericTypeArguments = ParseArgumentNames(genericTypeArgumentsString.Split(','));
318-
319-
yield return GetGenericTypeName(genericTypeName, genericTypeArguments);
320338
}
339+
340+
return parsedFunc;
321341
}
322342

323343
private static string GetTypeName(string typeFullName)
324344
{
325-
if (typeFullName.EndsWith("[]", StringComparison.Ordinal))
326-
{
327-
return GetTypeName(typeFullName.Substring(0, typeFullName.Length - 2)) + "[]";
328-
}
329-
330345
return typeFullName.GetSubstitutionOrNull() ??
331346
typeFullName.Substring(typeFullName.LastIndexOf('.') + 1);
332347
}
333348

334-
private static readonly int _nullableTypeArgumentStart = "System.Nullable`1[".Length;
335-
336-
private static string GetNullableTypeName(string typeFullName)
337-
{
338-
var nullableTypeNameLength = typeFullName.Length - _nullableTypeArgumentStart - 1;
339-
var nullableTypeName = typeFullName.Substring(_nullableTypeArgumentStart, nullableTypeNameLength);
340-
341-
return GetTypeName(nullableTypeName) + "?";
342-
}
343-
344-
private static string RemoveFinalGenericArgumentsClose(string genericTypeArgumentsString)
345-
{
346-
return genericTypeArgumentsString.Substring(0, genericTypeArgumentsString.Length - 1);
347-
}
348-
349-
private static string GetGenericTypeName(string typeName, IEnumerable<string> arguments)
350-
=> $"{typeName}<{string.Join(", ", arguments)}>";
351-
352349
#endregion
353350

354351
private static bool IsDefault<T>(ConstantExpression constant, out string translation)

0 commit comments

Comments
 (0)