Skip to content

Commit d1528b0

Browse files
committed
[FastClonerInclude]
1 parent b511854 commit d1528b0

File tree

9 files changed

+346
-106
lines changed

9 files changed

+346
-106
lines changed

src/FastCloner.SourceGenerator.Console/Program.cs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,47 @@
33
namespace FastCloner.SourceGenerator.Console;
44
using System;
55

6-
[FastClonerClonable]
7-
public partial class Person
6+
/*[FastClonerClonable]
7+
public class Person
88
{
99
public string Name { get; set; }
1010
public int Age { get; set; }
1111
public List<string> Hobbies { get; set; }
12+
}*/
13+
14+
[FastClonerClonable]
15+
[FastClonerSimulateNoRuntime]
16+
public class GenericClassWithConstraint<T>
17+
{
18+
public T Value { get; set; }
19+
}
20+
21+
public class SampleUnannotatedClass
22+
{
23+
public List<string> StringList { get; set; }
24+
}
25+
26+
[FastClonerClonable]
27+
public class GenericClassWithInclude<T>
28+
{
29+
public T Value { get; set; }
1230
}
1331

1432
class Program
1533
{
1634
static void Main(string[] args)
1735
{
18-
Person person = new Person
36+
var myTest = new GenericClassWithConstraint<Dictionary<string, SampleUnannotatedClass>>();
37+
38+
var original = new GenericClassWithConstraint<List<int>> { Value = new List<int> { 1, 2, 3 } };
39+
var clone = original.FastDeepClone();
40+
41+
/*Person person = new Person
1942
{
2043
Name = "John",
2144
Age = 30,
2245
Hobbies = new List<string> { "Reading", "Gaming" }
23-
};
46+
};*/
2447

2548
/*var clone = PersonClone.Clone(person);
2649

src/FastCloner.SourceGenerator.Shared/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
[assembly: InternalsVisibleTo("FastCloner.SourceGenerator")]
55
[assembly: InternalsVisibleTo("FastCloner.SourceGenerator.Analyzers")]
66
[assembly: InternalsVisibleTo("FastCloner.SourceGenerator.CodeFixes")]
7+
[assembly: InternalsVisibleTo("FastCloner.SourceGenerator.Console")]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
3+
namespace FastCloner.SourceGenerator.Shared;
4+
5+
/// <summary>
6+
/// Specifies types that should be supported for generic cloning even if they are not detected at compile time.
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
9+
public class FastClonerIncludeAttribute : Attribute
10+
{
11+
public Type[] Types { get; }
12+
13+
/// <summary>
14+
/// Register types to be included in the source generator analysis for this generic type.
15+
/// </summary>
16+
/// <param name="types">The types to include.</param>
17+
public FastClonerIncludeAttribute(params Type[] types)
18+
{
19+
Types = types;
20+
}
21+
}

src/FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3131

3232
var compilation = ctx.SemanticModel.Compilation;
3333

34-
if (TypeModelFactory.TryCreate(namedTypeSymbol, nullabilityEnabled, compilation, out var model, out var error))
35-
{
36-
return Result<TypeModel>.Success(model!);
37-
}
38-
else
39-
{
40-
return Result<TypeModel>.Error(error!);
41-
}
34+
return TypeModelFactory.TryCreate(namedTypeSymbol, nullabilityEnabled, compilation, out var model, out var error) ?
35+
Result<TypeModel>.Success(model!) :
36+
Result<TypeModel>.Error(error!);
4237
}
4338

4439
return Result<TypeModel>.Error(
@@ -54,21 +49,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5449
});
5550

5651
// Secondary pipeline: Collect usages of generic types to optimize dispatch
57-
var usagePipeline = context.SyntaxProvider.CreateSyntaxProvider(
52+
var explicitUsages = context.SyntaxProvider.CreateSyntaxProvider(
5853
predicate: GenericUsageCollector.IsCandidate,
5954
transform: GenericUsageCollector.Collect)
60-
.Where(x => x.Count > 0)
61-
.Collect()
62-
.Select(static (arrays, _) =>
55+
.Where(x => x.Count > 0);
56+
57+
var includedUsages = context.SyntaxProvider.ForAttributeWithMetadataName<EquatableArray<GenericUsage>>(
58+
fullyQualifiedMetadataName: "FastCloner.SourceGenerator.Shared.FastClonerIncludeAttribute",
59+
predicate: static (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax,
60+
transform: IncludeAttributeCollector.Collect)
61+
.Where(x => x.Count > 0);
62+
63+
var usagePipeline = explicitUsages.Collect().Combine(includedUsages.Collect())
64+
.Select(static (pair, _) =>
6365
{
66+
var (explicitList, includedList) = pair;
6467
var list = new System.Collections.Generic.List<GenericUsage>();
65-
foreach (var array in arrays)
66-
{
67-
foreach (var usage in array)
68-
{
69-
list.Add(usage);
70-
}
71-
}
68+
69+
foreach (var array in explicitList)
70+
list.AddRange(array);
71+
foreach (var array in includedList)
72+
list.AddRange(array);
73+
7274
return new EquatableArray<GenericUsage>(list.Distinct().ToArray());
7375
});
7476

@@ -91,7 +93,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
9193
var generator = new CloneCodeGenerator(model, usages);
9294
var generatedSource = generator.Generate();
9395

94-
ctx.AddSource($"{model.Name}FastDeepClone.g.cs", SourceText.From(generatedSource, Encoding.UTF8));
96+
// Use FullyQualifiedName to avoid collisions when same class name exists in different namespaces
97+
var safeName = model.FullyQualifiedName
98+
.Replace("global::", "")
99+
.Replace(".", "_")
100+
.Replace("<", "_")
101+
.Replace(">", "_")
102+
.Replace(" ", "")
103+
.Replace(",", "_")
104+
.Replace(":", "_");
105+
106+
ctx.AddSource($"{safeName}_FastDeepClone.g.cs", SourceText.From(generatedSource, Encoding.UTF8));
95107
}
96108
catch (System.Exception ex)
97109
{
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
5+
namespace FastCloner.SourceGenerator;
6+
7+
internal static class GenericTypeAnalyzer
8+
{
9+
public static GenericUsage? Analyze(
10+
INamedTypeSymbol targetSymbol,
11+
ITypeSymbol typeArg,
12+
Compilation compilation,
13+
bool nullability)
14+
{
15+
if (typeArg.TypeKind == TypeKind.TypeParameter)
16+
return null;
17+
18+
var isSafe = TypeAnalyzer.IsSafeType(typeArg, compilation);
19+
var isClonable = TypeAnalyzer.HasClonableAttribute(typeArg);
20+
21+
// Analyze for collection types (List, Dictionary, Array, etc.)
22+
var nestedTypes = new Dictionary<string, MemberModel>();
23+
NestedTypeCollector.Collect(typeArg, compilation, nullability, nestedTypes);
24+
25+
MemberModel? collectionModel = null;
26+
var typeArgFQN = typeArg.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
27+
if (nestedTypes.TryGetValue(typeArgFQN, out var model))
28+
{
29+
collectionModel = model;
30+
}
31+
32+
// Analyze for implicit types (POCOs without attribute)
33+
var implicitTypes = new Dictionary<string, TypeModel>();
34+
var implicitCache = new Dictionary<ITypeSymbol, TypeModel?>(SymbolEqualityComparer.Default);
35+
var processingStack = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
36+
37+
void CollectImplicitRecursively(ITypeSymbol t)
38+
{
39+
// Skip safe types, they don't need implicit helpers
40+
if (TypeAnalyzer.IsSafeType(t, compilation))
41+
return;
42+
43+
// Check if this type is a candidate for implicit cloning
44+
if (ImplicitTypeAnalyzer.TryAnalyze(t, compilation, nullability, implicitCache, processingStack, out var implicitModel))
45+
{
46+
if (implicitModel != null && !implicitTypes.ContainsKey(implicitModel.FullyQualifiedName))
47+
{
48+
implicitTypes[implicitModel.FullyQualifiedName] = implicitModel;
49+
// Dependencies are already collected in 'implicitModel.RelatedTypes', but we need to flatten them into our list
50+
foreach (var rel in implicitModel.RelatedTypes)
51+
{
52+
if (!implicitTypes.ContainsKey(rel.FullyQualifiedName))
53+
implicitTypes[rel.FullyQualifiedName] = rel;
54+
}
55+
}
56+
}
57+
58+
// Recurse into generics/arrays to find other candidates
59+
if (t is INamedTypeSymbol named && named.IsGenericType)
60+
{
61+
foreach (var arg in named.TypeArguments)
62+
CollectImplicitRecursively(arg);
63+
}
64+
else if (t is IArrayTypeSymbol array)
65+
{
66+
CollectImplicitRecursively(array.ElementType);
67+
}
68+
}
69+
70+
CollectImplicitRecursively(typeArg);
71+
72+
// We capture if it's Safe, Clonable, a supported Collection/Dictionary, or has Implicit types
73+
if (isSafe || isClonable || collectionModel != null || implicitTypes.Count > 0)
74+
{
75+
string? extensionClassFQN = null;
76+
if (isClonable && typeArg is INamedTypeSymbol namedArg)
77+
{
78+
var ns = TypeAnalyzer.GetNamespace(namedArg);
79+
var name = namedArg.Name;
80+
var extName = $"{name}FastDeepCloneExtensions";
81+
extensionClassFQN = string.IsNullOrEmpty(ns) ? $"global::{extName}" : $"global::{ns}.{extName}";
82+
}
83+
84+
return new GenericUsage(
85+
targetSymbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
86+
typeArgFQN,
87+
extensionClassFQN,
88+
collectionModel,
89+
new EquatableArray<MemberModel>(nestedTypes.Values.ToArray()),
90+
new EquatableArray<TypeModel>(implicitTypes.Values.ToArray()),
91+
isSafe,
92+
isClonable
93+
);
94+
}
95+
96+
return null;
97+
}
98+
}

src/FastCloner.SourceGenerator/GenericUsageCollector.cs

Lines changed: 6 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -29,91 +29,15 @@ public static EquatableArray<GenericUsage> Collect(GeneratorSyntaxContext contex
2929
var usages = new List<GenericUsage>();
3030
var compilation = context.SemanticModel.Compilation;
3131

32+
// Nullability context at usage site
33+
var nullability = context.SemanticModel.GetNullableContext(node.SpanStart).HasFlag(NullableContext.Enabled);
34+
3235
foreach (var typeArg in symbol.TypeArguments)
3336
{
34-
// We need to capture the concrete type used as argument.
35-
// If typeArg is still a type parameter (e.g. inside a generic method), we can't optimize it.
36-
37-
if (typeArg.TypeKind == TypeKind.TypeParameter)
38-
continue;
39-
40-
var isSafe = TypeAnalyzer.IsSafeType(typeArg, compilation);
41-
var isClonable = TypeAnalyzer.HasClonableAttribute(typeArg);
42-
43-
// Analyze for collection types (List, Dictionary, Array, etc.)
44-
var nestedTypes = new Dictionary<string, MemberModel>();
45-
var nullability = context.SemanticModel.GetNullableContext(node.SpanStart).HasFlag(NullableContext.Enabled);
46-
NestedTypeCollector.Collect(typeArg, compilation, nullability, nestedTypes);
47-
48-
MemberModel? collectionModel = null;
49-
var typeArgFQN = typeArg.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
50-
if (nestedTypes.TryGetValue(typeArgFQN, out var model))
51-
{
52-
collectionModel = model;
53-
}
54-
55-
// Analyze for implicit types (POCOs without attribute)
56-
var implicitTypes = new Dictionary<string, TypeModel>();
57-
var implicitCache = new Dictionary<ITypeSymbol, TypeModel?>(SymbolEqualityComparer.Default);
58-
var processingStack = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
59-
60-
void CollectImplicitRecursively(ITypeSymbol t)
37+
var usage = GenericTypeAnalyzer.Analyze(symbol, typeArg, compilation, nullability);
38+
if (usage.HasValue)
6139
{
62-
// Skip safe types, they don't need implicit helpers
63-
if (TypeAnalyzer.IsSafeType(t, compilation))
64-
return;
65-
66-
// Check if this type is a candidate for implicit cloning
67-
if (ImplicitTypeAnalyzer.TryAnalyze(t, compilation, nullability, implicitCache, processingStack, out var implicitModel))
68-
{
69-
if (implicitModel != null && !implicitTypes.ContainsKey(implicitModel.FullyQualifiedName))
70-
{
71-
implicitTypes[implicitModel.FullyQualifiedName] = implicitModel;
72-
// Dependencies are already collected in 'implicitModel.RelatedTypes', but we need to flatten them into our list
73-
foreach (var rel in implicitModel.RelatedTypes)
74-
{
75-
if (!implicitTypes.ContainsKey(rel.FullyQualifiedName))
76-
implicitTypes[rel.FullyQualifiedName] = rel;
77-
}
78-
}
79-
}
80-
81-
// Recurse into generics/arrays to find other candidates
82-
if (t is INamedTypeSymbol named && named.IsGenericType)
83-
{
84-
foreach (var arg in named.TypeArguments)
85-
CollectImplicitRecursively(arg);
86-
}
87-
else if (t is IArrayTypeSymbol array)
88-
{
89-
CollectImplicitRecursively(array.ElementType);
90-
}
91-
}
92-
93-
CollectImplicitRecursively(typeArg);
94-
95-
// We capture if it's Safe, Clonable, a supported Collection/Dictionary, or has Implicit types
96-
if (isSafe || isClonable || collectionModel != null || implicitTypes.Count > 0)
97-
{
98-
string? extensionClassFQN = null;
99-
if (isClonable && typeArg is INamedTypeSymbol namedArg)
100-
{
101-
var ns = TypeAnalyzer.GetNamespace(namedArg);
102-
var name = namedArg.Name;
103-
var extName = $"{name}FastDeepCloneExtensions";
104-
extensionClassFQN = string.IsNullOrEmpty(ns) ? $"global::{extName}" : $"global::{ns}.{extName}";
105-
}
106-
107-
usages.Add(new GenericUsage(
108-
symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
109-
typeArgFQN,
110-
extensionClassFQN,
111-
collectionModel,
112-
new EquatableArray<MemberModel>(nestedTypes.Values.ToArray()),
113-
new EquatableArray<TypeModel>(implicitTypes.Values.ToArray()),
114-
isSafe,
115-
isClonable
116-
));
40+
usages.Add(usage.Value);
11741
}
11842
}
11943

0 commit comments

Comments
 (0)