Skip to content

Commit edbcb5a

Browse files
committed
Enhance MatchTypeInfo and PropertySelection
Added multiple constructors to MatchTypeInfo for flexible initialization. Introduced FlattenedPropertyPath and ToEnumerablePath methods in PropertySelection for better property path handling. Fixed a bug in PropertySelection's Select method. Enhanced TypeDescriptor for accurate type name representation and added a static Object property. Added extension methods for extracting strings from AttributeSyntax. Implemented TryReadStringValue for parsing syntax trees. Updated project version to 0.1.8.
1 parent 9860d30 commit edbcb5a

5 files changed

Lines changed: 202 additions & 4 deletions

File tree

RoyalCode.Utils/RoyalCode.Extensions.SourceGenerator/Descriptors/PropertySelection/MatchTypeInfo.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,52 @@ public MatchTypeInfo(
2828
Options = options;
2929
}
3030

31+
/// <summary>
32+
/// Initializes a new instance of the MatchTypeInfo class for the specified target type descriptor.
33+
/// </summary>
34+
/// <remarks>
35+
/// The constructed instance uses the default match options and retrieves the target type's
36+
/// properties using the default property retriever.
37+
/// This ensures consistent matching behavior across instances.
38+
/// </remarks>
39+
/// <param name="targetType">
40+
/// The type descriptor representing the target type for which matching information will be constructed.
41+
/// Cannot be null.
42+
/// </param>
43+
public MatchTypeInfo(TypeDescriptor targetType)
44+
{
45+
Type = targetType;
46+
Properties = MatchOptions.Default.TargetPropertiesRetriever.GetProperties(targetType);
47+
Options = MatchOptions.Default;
48+
}
49+
50+
/// <summary>
51+
/// Initializes a new instance of the MatchTypeInfo class with the specified type descriptor and property
52+
/// descriptors.
53+
/// </summary>
54+
/// <param name="type">The type descriptor representing the type to be matched.</param>
55+
/// <param name="properties">A read-only list of property descriptors that define the properties associated with the type.</param>
56+
public MatchTypeInfo(TypeDescriptor type, IReadOnlyList<PropertyDescriptor> properties)
57+
{
58+
Type = type;
59+
Properties = properties;
60+
Options = MatchOptions.Default;
61+
}
62+
63+
/// <summary>
64+
/// Initializes a new instance of the MatchTypeInfo class using the specified type descriptor and matching options.
65+
/// </summary>
66+
/// <param name="targetType">The type descriptor representing the target type for which property matching information will be generated.
67+
/// Cannot be null.</param>
68+
/// <param name="options">The options that configure property matching behavior, including how target properties are retrieved. Cannot be
69+
/// null.</param>
70+
public MatchTypeInfo(TypeDescriptor targetType, MatchOptions options)
71+
{
72+
Type = targetType;
73+
Properties = options.TargetPropertiesRetriever.GetProperties(targetType);
74+
Options = options;
75+
}
76+
3177
/// <summary>
3278
/// The type descriptor of the current type.
3379
/// </summary>

RoyalCode.Utils/RoyalCode.Extensions.SourceGenerator/Descriptors/PropertySelection/PropertySelection.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,62 @@ public void WritePropertyPath(StringBuilder sb)
4141
sb.Append(property.Name);
4242
}
4343

44+
/// <summary>
45+
/// Returns a flattened string representation of the property path by concatenating the names of all properties in
46+
/// the path.
47+
/// </summary>
48+
/// <examples>
49+
/// Given a property path like "Address.Street.Name", this method will return "AddressStreetName".
50+
/// </examples>
51+
/// <returns>
52+
/// A <see cref="string"/> that represents the flattened property path.
53+
/// </returns>
54+
public string FlattenedPropertyPath()
55+
{
56+
StringBuilder sb = new();
57+
foreach (var p in ToEnumerablePath())
58+
{
59+
sb.Append(p.property.Name);
60+
}
61+
return sb.ToString();
62+
}
63+
64+
/// <summary>
65+
/// Returns an enumerable sequence representing the path from the root to the current property selection.
66+
/// </summary>
67+
/// <remarks>
68+
/// The returned sequence includes all ancestor property selections, followed by the current instance.
69+
/// This can be used to traverse or analyze the full selection path in hierarchical scenarios.
70+
/// </remarks>
71+
/// <returns>
72+
/// An <see cref="IEnumerable{PropertySelection}"/> containing each property selection in the path,
73+
/// ordered from the root to the current instance.
74+
/// </returns>
75+
public IEnumerable<PropertySelection> ToEnumerablePath()
76+
{
77+
if (Parent is not null)
78+
{
79+
foreach (var p in Parent.ToEnumerablePath())
80+
yield return p;
81+
}
82+
yield return this;
83+
}
84+
4485
public static PropertySelection? Select(PropertyDescriptor property, MatchTypeInfo targetType)
86+
=> Select(property.Name, targetType);
87+
88+
public static PropertySelection? Select(string propertyName, MatchTypeInfo targetType)
4589
{
4690
PropertySelection? ps = null;
4791

48-
var targetProperty = targetType.Properties.FirstOrDefault(p => p.Name == property.Name);
92+
var targetProperty = targetType.Properties.FirstOrDefault(p => p.Name == propertyName);
4993

5094
if (targetProperty != null)
5195
{
5296
return new PropertySelection(targetProperty);
5397
}
5498

55-
var parts = property.Name.SplitUpperCase();
99+
var parts = propertyName.SplitUpperCase();
56100
if (parts is not null)
57101
{
58102
ps = SelectPart(parts, targetType);

RoyalCode.Utils/RoyalCode.Extensions.SourceGenerator/Descriptors/TypeDescriptor.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public static TypeDescriptor Create(ITypeSymbol typeSymbol)
3333
{
3434
var name = typeSymbol.ToString();
3535
bool isNullable = false;
36-
3736
var namedTypeSymbol = typeSymbol as INamedTypeSymbol;
3837

3938
if (name[name.Length - 1] == '?')
@@ -53,6 +52,10 @@ public static TypeDescriptor Create(ITypeSymbol typeSymbol)
5352
{
5453
name = namedTypeSymbol.GetName();
5554
}
55+
else
56+
{
57+
name = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
58+
}
5659
}
5760

5861
return new(name, namespaces, typeSymbol, isNullable);
@@ -112,6 +115,8 @@ public static TypeDescriptor Void()
112115
return voidTypeDescriptor;
113116
}
114117

118+
public static TypeDescriptor Object { get; } = new("object", ["System"]);
119+
115120
#endregion
116121

117122
private List<string>? hints;

RoyalCode.Utils/RoyalCode.Extensions.SourceGenerator/ExtensionMethods.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,4 +564,107 @@ public static StringBuilder AppendPropertyPath(this StringBuilder sb, PropertySe
564564
property.WritePropertyPath(sb);
565565
return sb;
566566
}
567+
568+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
569+
public static IEnumerable<string> GetConstructorStrings(this AttributeSyntax attr)
570+
{
571+
if (attr.ArgumentList is not { Arguments.Count: > 0 }) yield break;
572+
foreach (var arg in attr.ArgumentList.Arguments)
573+
{
574+
if (arg.NameEquals is not null) continue; // skip named
575+
if (TryReadStringValue(arg.Expression, out var value))
576+
yield return value;
577+
else if (arg.Expression is ArrayCreationExpressionSyntax acs && acs.Initializer is not null)
578+
{
579+
foreach (var expr in acs.Initializer.Expressions)
580+
if (TryReadStringValue(expr, out var v)) yield return v;
581+
}
582+
else if (arg.Expression is ImplicitArrayCreationExpressionSyntax iacs && iacs.Initializer is not null)
583+
{
584+
foreach (var expr in iacs.Initializer.Expressions)
585+
if (TryReadStringValue(expr, out var v)) yield return v;
586+
}
587+
else if (arg.Expression is InitializerExpressionSyntax init)
588+
{
589+
foreach (var expr in init.Expressions)
590+
if (TryReadStringValue(expr, out var v)) yield return v;
591+
}
592+
}
593+
}
594+
595+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
596+
public static IEnumerable<string> GetNamedArgumentStrings(this AttributeSyntax attr, string name)
597+
{
598+
if (attr.ArgumentList is not { Arguments.Count: > 0 }) yield break;
599+
foreach (var arg in attr.ArgumentList.Arguments)
600+
{
601+
if (arg.NameEquals?.Name.Identifier.Text != name) continue;
602+
var expr = arg.Expression;
603+
if (TryReadStringValue(expr, out var value))
604+
{
605+
yield return value;
606+
continue;
607+
}
608+
if (expr is ArrayCreationExpressionSyntax acs && acs.Initializer is not null)
609+
{
610+
foreach (var item in acs.Initializer.Expressions)
611+
if (TryReadStringValue(item, out var v)) yield return v;
612+
}
613+
else if (expr is ImplicitArrayCreationExpressionSyntax iacs && iacs.Initializer is not null)
614+
{
615+
foreach (var item in iacs.Initializer.Expressions)
616+
if (TryReadStringValue(item, out var v)) yield return v;
617+
}
618+
else if (expr is InitializerExpressionSyntax init)
619+
{
620+
foreach (var item in init.Expressions)
621+
if (TryReadStringValue(item, out var v)) yield return v;
622+
}
623+
}
624+
}
625+
626+
/// <summary>
627+
/// <para>
628+
/// Attempts to extract a string value from the specified expression syntax node.
629+
/// </para>
630+
/// </summary>
631+
/// <remarks>
632+
/// This method supports string literal expressions and 'nameof' invocations with a single argument.
633+
/// For unsupported expression types, the method returns false and sets <paramref name="value"/>
634+
/// to an empty string.
635+
/// </remarks>
636+
/// <param name="expr">
637+
/// The expression syntax node to analyze for a string value.
638+
/// Supported expressions include string literals and 'nameof' invocations.
639+
/// </param>
640+
/// <param name="value">
641+
/// When this method returns <see langword="true"/>, contains the extracted string value;
642+
/// otherwise, contains an empty string.
643+
/// </param>
644+
/// <returns>
645+
/// true if a string value was successfully extracted from the expression; otherwise, false.
646+
/// </returns>
647+
public static bool TryReadStringValue(this ExpressionSyntax expr, out string value)
648+
{
649+
switch (expr)
650+
{
651+
case LiteralExpressionSyntax { Token.Value: string s }:
652+
value = s; return true;
653+
case InvocationExpressionSyntax { Expression: IdentifierNameSyntax id } inv when id.Identifier.Text == "nameof":
654+
if (inv.ArgumentList.Arguments.Count == 1)
655+
{
656+
var inner = inv.ArgumentList.Arguments[0].Expression;
657+
if (inner is IdentifierNameSyntax ins)
658+
{
659+
value = ins.Identifier.Text; return true;
660+
}
661+
else if (inner is MemberAccessExpressionSyntax ma)
662+
{
663+
value = ma.Name.Identifier.Text; return true;
664+
}
665+
}
666+
break;
667+
}
668+
value = string.Empty; return false;
669+
}
567670
}

RoyalCode.Utils/RoyalCode.Extensions.SourceGenerator/RoyalCode.Extensions.SourceGenerator.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<TargetFramework>netstandard2.0</TargetFramework>
77
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
88

9-
<Ver>0.1.7</Ver>
9+
<Ver>0.1.8</Ver>
1010
<Prev></Prev>
1111

1212
<Version>$(Ver)$(Prev)</Version>

0 commit comments

Comments
 (0)