Skip to content

Commit 44ff5b1

Browse files
authored
Adds the NameOfResolver support to Flatten Exclude (#284)
* Support nameof(@Object.Member.Submember) for full path nameof * Added test cases to excersise using the nameof(@Object.Member.SubMember) functionality * fullpath nameof support using the @ sign for FlattenAttribute Exclude
1 parent 05f5942 commit 44ff5b1

File tree

2 files changed

+146
-2
lines changed

2 files changed

+146
-2
lines changed

src/Facet/Generators/FlattenGenerators/FlattenModelBuilder.cs

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using Facet.Generators.Shared;
22
using Facet.Generators;
33
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
46
using System.Collections.Generic;
57
using System.Collections.Immutable;
68
using System.Linq;
@@ -30,7 +32,7 @@ internal static class FlattenModelBuilder
3032
if (sourceType == null) return null;
3133

3234
// Extract attribute parameters
33-
var excludedPaths = ExtractExcludedPaths(attribute);
35+
var excludedPaths = ExtractExcludedPaths(attribute, context);
3436
var maxDepth = GetNamedArg(attribute.NamedArguments, "MaxDepth", 3);
3537
var namingStrategy = GetNamedArg(attribute.NamedArguments, "NamingStrategy", FlattenNamingStrategy.Prefix);
3638
var includeFields = GetNamedArg(attribute.NamedArguments, "IncludeFields", false);
@@ -88,10 +90,137 @@ internal static class FlattenModelBuilder
8890
maxDepth);
8991
}
9092

91-
private static HashSet<string> ExtractExcludedPaths(AttributeData attribute)
93+
private static HashSet<string> ExtractExcludedPaths(AttributeData attribute, GeneratorAttributeSyntaxContext? context = null)
9294
{
9395
var excluded = new HashSet<string>();
9496

97+
// Get the source type name for stripping from full nameof paths
98+
var sourceType = attribute.ConstructorArguments[0].Value as INamedTypeSymbol;
99+
var sourceTypeName = sourceType?.Name ?? string.Empty;
100+
101+
// Helper to process a single expression and add to set
102+
void ProcessExpression(ExpressionSyntax expr)
103+
{
104+
if (expr == null) return;
105+
106+
try
107+
{
108+
var (resolved, hadLeadingAt) = NameOfResolver.ResolveExpression(expr);
109+
if (!string.IsNullOrEmpty(resolved))
110+
{
111+
// If @ was used, the path includes the source type name, so strip it
112+
// e.g., @Company.HeadquartersAddress.ZipCode -> HeadquartersAddress.ZipCode
113+
var path = resolved!;
114+
if (hadLeadingAt && !string.IsNullOrEmpty(sourceTypeName) && path.StartsWith(sourceTypeName + "."))
115+
{
116+
path = path.Substring(sourceTypeName.Length + 1);
117+
}
118+
excluded.Add(path);
119+
return;
120+
}
121+
}
122+
catch
123+
{
124+
// ignore and fallback
125+
}
126+
127+
// Prefer literal extraction for string literals
128+
if (expr is LiteralExpressionSyntax lit && lit.IsKind(SyntaxKind.StringLiteralExpression))
129+
{
130+
var val = lit.Token.ValueText;
131+
if (!string.IsNullOrEmpty(val)) excluded.Add(val);
132+
return;
133+
}
134+
135+
// Handle array/initializer or other expressions by textual fallback (strip quotes)
136+
var text = expr.ToString().Trim();
137+
if (text.Length == 0) return;
138+
139+
if ((text.StartsWith("@\"") && text.EndsWith("\"")) || (text.StartsWith("\"") && text.EndsWith("\"")))
140+
{
141+
var firstQuote = text.IndexOf('"');
142+
if (firstQuote >= 0 && text.Length - firstQuote - 2 > 0)
143+
text = text.Substring(firstQuote + 1, text.Length - firstQuote - 2);
144+
else
145+
text = string.Empty;
146+
}
147+
148+
if (!string.IsNullOrEmpty(text)) excluded.Add(text);
149+
}
150+
151+
// Try to resolve from syntax if context is available
152+
if (context != null && attribute.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax attrSyntax)
153+
{
154+
// Try positional argument first (constructor params)
155+
var positionalArgs = attrSyntax.ArgumentList?.Arguments.Where(a => a.NameEquals == null && a.NameColon == null).ToList();
156+
if (positionalArgs != null && positionalArgs.Count > 1)
157+
{
158+
// Second positional argument is the exclude array
159+
var excludeArg = positionalArgs[1];
160+
var expr = excludeArg.Expression;
161+
InitializerExpressionSyntax? initializer = null;
162+
switch (expr)
163+
{
164+
case ImplicitArrayCreationExpressionSyntax implicitArray:
165+
initializer = implicitArray.Initializer;
166+
break;
167+
case ArrayCreationExpressionSyntax arrayCreation:
168+
initializer = arrayCreation.Initializer;
169+
break;
170+
case InitializerExpressionSyntax directInit:
171+
initializer = directInit;
172+
break;
173+
}
174+
175+
if (initializer != null)
176+
{
177+
foreach (var e in initializer.Expressions)
178+
ProcessExpression(e);
179+
return excluded;
180+
}
181+
}
182+
183+
// Try named argument (Exclude = ...)
184+
var excludeNamed = attrSyntax.ArgumentList?.Arguments.FirstOrDefault(a => (a.NameEquals?.Name.Identifier.ValueText == "Exclude") || (a.NameColon?.Name.Identifier.ValueText == "Exclude"));
185+
if (excludeNamed != null)
186+
{
187+
var expr = excludeNamed.Expression;
188+
InitializerExpressionSyntax? initializer = null;
189+
190+
switch (expr)
191+
{
192+
case ImplicitArrayCreationExpressionSyntax implicitArray:
193+
initializer = implicitArray.Initializer;
194+
break;
195+
case ArrayCreationExpressionSyntax arrayCreation:
196+
initializer = arrayCreation.Initializer;
197+
break;
198+
case InitializerExpressionSyntax directInit:
199+
initializer = directInit;
200+
break;
201+
case CollectionExpressionSyntax collectionExpr:
202+
// Handle C# 12 collection expression syntax: [item1, item2, ...]
203+
foreach (var element in collectionExpr.Elements)
204+
{
205+
if (element is ExpressionElementSyntax exprElem)
206+
{
207+
ProcessExpression(exprElem.Expression);
208+
}
209+
}
210+
return excluded;
211+
}
212+
213+
if (initializer != null)
214+
{
215+
foreach (var e in initializer.Expressions)
216+
ProcessExpression(e);
217+
return excluded;
218+
}
219+
}
220+
221+
}
222+
223+
// Fallback to compiled attribute data
95224
// Check constructor argument (params string[] exclude)
96225
if (attribute.ConstructorArguments.Length > 1)
97226
{

test/Facet.Tests/TestModels/FlattenTestModels.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,18 @@ public partial class EntityWithVariousCollectionsFlatDto
492492
{
493493
// All collection types should be included as-is
494494
}
495+
// Test models for nameof resolution in Flatten Exclude
496+
// Version 2: Fixed to use CollectionExpressionSyntax support
497+
[Flatten(typeof(Company), Exclude = [nameof(@Company.HeadquartersAddress.ZipCode)])]
498+
public partial class CompanyFlatWithNameOfExcludeDto
499+
{
500+
// Should exclude the ZipCode nested property using nameof
501+
}
502+
503+
[Flatten(typeof(Person), Exclude = [nameof(@Person.Address.Country)])]
504+
public partial class PersonFlatWithNameOfExcludeDto
505+
{
506+
// Should exclude nested Country properties using nameof
507+
}
508+
509+

0 commit comments

Comments
 (0)