Skip to content

Commit c84d4ed

Browse files
Now Grouping fields by containing class
1 parent 9147069 commit c84d4ed

File tree

5 files changed

+162
-120
lines changed

5 files changed

+162
-120
lines changed

src/ThunderDesign.Net-PCL.SourceGenerators/BindablePropertyGenerator.cs

Lines changed: 87 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
2020
)
2121
.Where(static info => !info.Equals(default(BindableFieldInfo)));
2222

23+
// Group fields by containing class
24+
var grouped = fieldsWithAttribute.Collect()
25+
.Select((list, _) => list
26+
.GroupBy(info => info.ContainingClass, SymbolEqualityComparer.Default)
27+
.Select(g => (ClassSymbol: g.Key, Fields: g.ToList()))
28+
.ToList()
29+
);
30+
2331
var compilationProvider = context.CompilationProvider;
2432

25-
context.RegisterSourceOutput(fieldsWithAttribute.Combine(compilationProvider), (spc, tuple) =>
33+
context.RegisterSourceOutput(grouped.Combine(compilationProvider), (spc, tuple) =>
2634
{
27-
var (info, compilation) = (tuple.Left, tuple.Right);
28-
GenerateBindableProperty(spc, info, compilation);
35+
var (classGroups, compilation) = (tuple.Left, tuple.Right);
36+
foreach (var group in classGroups)
37+
{
38+
var classSymbol = group.ClassSymbol as INamedTypeSymbol;
39+
var fields = group.Fields;
40+
if (classSymbol != null)
41+
{
42+
GenerateBindablePropertyClass(spc, classSymbol, fields, compilation);
43+
}
44+
}
2945
});
3046
}
3147

@@ -58,41 +74,13 @@ private static BindableFieldInfo GetBindableField(GeneratorSyntaxContext context
5874
return default(BindableFieldInfo);
5975
}
6076

61-
private static void GenerateBindableProperty(SourceProductionContext context, BindableFieldInfo info, Compilation compilation)
77+
// New method to generate all properties and shared members for a class
78+
private static void GenerateBindablePropertyClass(
79+
SourceProductionContext context,
80+
INamedTypeSymbol classSymbol,
81+
List<BindableFieldInfo> fields,
82+
Compilation compilation)
6283
{
63-
var classSymbol = info.ContainingClass;
64-
var fieldSymbol = info.FieldSymbol;
65-
var fieldName = fieldSymbol.Name;
66-
var propertyName = PropertyGeneratorHelpers.ToPropertyName(fieldName);
67-
var typeName = fieldSymbol.Type.ToDisplayString();
68-
69-
// Rule 1: Class must be partial
70-
if (!PropertyGeneratorHelpers.IsPartial(classSymbol))
71-
{
72-
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Class '{classSymbol.Name}' must be partial to use [BindableProperty].");
73-
return;
74-
}
75-
76-
// Rule 2: Field must start with "_" followed by a letter, or a lowercase letter
77-
if (!PropertyGeneratorHelpers.IsValidFieldName(fieldName))
78-
{
79-
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Field '{fieldName}' must start with '_' followed by a letter, or a lowercase letter to use [BindableProperty].");
80-
return;
81-
}
82-
83-
// Rule 3: Property must not already exist
84-
if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName))
85-
{
86-
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Property '{propertyName}' already exists in '{classSymbol.Name}'.");
87-
return;
88-
}
89-
90-
// Attribute arguments
91-
var threadSafe = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
92-
var notify = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
93-
var readOnly = info.AttributeData.ConstructorArguments.Length > 2 && (bool)info.AttributeData.ConstructorArguments[2].Value!;
94-
95-
// Check for INotifyPropertyChanged, IBindableObject, ThreadObject
9684
var implementsINotify = ImplementsInterface(classSymbol, "System.ComponentModel.INotifyPropertyChanged");
9785
var implementsIBindable = ImplementsInterface(classSymbol, "ThunderDesign.Net.Threading.Interfaces.IBindableObject");
9886
var inheritsThreadObject = PropertyGeneratorHelpers.InheritsFrom(classSymbol, "ThunderDesign.Net.Threading.Objects.ThreadObject");
@@ -105,17 +93,13 @@ private static void GenerateBindableProperty(SourceProductionContext context, Bi
10593
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace ? null : classSymbol.ContainingNamespace.ToDisplayString();
10694

10795
if (!string.IsNullOrEmpty(ns))
108-
{
10996
source.AppendLine($"namespace {ns} {{");
110-
}
11197

11298
source.AppendLine("using ThunderDesign.Net.Threading.Extentions;");
11399
source.AppendLine("using ThunderDesign.Net.Threading.Interfaces;");
114100
source.AppendLine("using ThunderDesign.Net.Threading.Objects;");
115101

116-
source.AppendLine($"partial class {classSymbol.Name}");
117-
118-
// Add interface if needed
102+
source.Append($"partial class {classSymbol.Name}");
119103
var interfaces = new List<string>();
120104
if (!implementsIBindable)
121105
interfaces.Add("IBindableObject");
@@ -126,20 +110,7 @@ private static void GenerateBindableProperty(SourceProductionContext context, Bi
126110

127111
// Add event if needed
128112
if (!implementsINotify && !PropertyGeneratorHelpers.EventExists(classSymbol, "PropertyChanged", propertyChangedEventType))
129-
{
130-
if (PropertyGeneratorHelpers.EventExists(classSymbol, "PropertyChanged"))
131-
{
132-
PropertyGeneratorHelpers.ReportDiagnostic(
133-
context,
134-
info.FieldDeclaration.GetLocation(),
135-
$"Event PropertyChanged already exists in '{classSymbol.Name}' with a different type. Expected: System.ComponentModel.PropertyChangedEventHandler."
136-
);
137-
}
138-
else
139-
{
140-
source.AppendLine(" public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
141-
}
142-
}
113+
source.AppendLine(" public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
143114

144115
// Add _Locker if needed
145116
if (!inheritsThreadObject && !PropertyGeneratorHelpers.FieldExists(classSymbol, "_Locker"))
@@ -159,33 +130,82 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
159130
}");
160131
}
161132

162-
// Add property
163-
var lockerArg = threadSafe ? "_Locker" : "null";
164-
var notifyArg = notify ? "true" : "false";
165-
if (readOnly)
133+
// Generate all properties
134+
foreach (var info in fields)
166135
{
167-
source.AppendLine($@"
136+
var fieldSymbol = info.FieldSymbol;
137+
var fieldName = fieldSymbol.Name;
138+
var propertyName = PropertyGeneratorHelpers.ToPropertyName(fieldName);
139+
var typeName = fieldSymbol.Type.ToDisplayString();
140+
141+
var readOnly = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
142+
var threadSafe = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
143+
var notify = info.AttributeData.ConstructorArguments.Length > 2 && (bool)info.AttributeData.ConstructorArguments[2].Value!;
144+
string[] alsoNotify = null;
145+
if (info.AttributeData.ConstructorArguments.Length > 3)
146+
{
147+
var arg = info.AttributeData.ConstructorArguments[3];
148+
if (arg.Kind == TypedConstantKind.Array && arg.Values != null)
149+
{
150+
alsoNotify = arg.Values
151+
.Select(tc => tc.Value as string)
152+
.Where(s => !string.IsNullOrEmpty(s))
153+
.ToArray();
154+
}
155+
}
156+
if (alsoNotify == null)
157+
alsoNotify = new string[0];
158+
159+
var lockerArg = threadSafe ? "_Locker" : "null";
160+
var notifyArg = notify ? "true" : "false";
161+
if (readOnly)
162+
{
163+
source.AppendLine($@"
168164
public {typeName} {propertyName}
169165
{{
170166
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
171167
}}");
172-
}
173-
else
174-
{
175-
source.AppendLine($@"
168+
}
169+
else
170+
{
171+
string setAccessor;
172+
if (alsoNotify.Length > 0)
173+
{
174+
var notifyCalls = new StringBuilder();
175+
foreach (var prop in alsoNotify)
176+
{
177+
if (!string.IsNullOrEmpty(prop))
178+
notifyCalls.AppendLine($" this.OnPropertyChanged(\"{prop}\");");
179+
}
180+
setAccessor = $@"
181+
set
182+
{{
183+
if (this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}))
184+
{{
185+
{notifyCalls.ToString().TrimEnd()}
186+
}}
187+
}}";
188+
}
189+
else
190+
{
191+
setAccessor = $"set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}); }}";
192+
}
193+
194+
source.AppendLine($@"
176195
public {typeName} {propertyName}
177196
{{
178197
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
179-
set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}); }}
198+
{setAccessor}
180199
}}");
200+
}
181201
}
182202

183203
source.AppendLine("}");
184204

185205
if (!string.IsNullOrEmpty(ns))
186206
source.AppendLine("}");
187207

188-
context.AddSource($"{classSymbol.Name}_{propertyName}_BindableProperty.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
208+
context.AddSource($"{classSymbol.Name}_BindableProperties.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
189209
}
190210

191211
private static bool ImplementsInterface(INamedTypeSymbol type, string interfaceName)

src/ThunderDesign.Net-PCL.SourceGenerators/PropertyGenerator.cs

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.CodeAnalysis;
22
using Microsoft.CodeAnalysis.CSharp.Syntax;
33
using Microsoft.CodeAnalysis.Text;
4+
using System.Collections.Generic;
45
using System.Linq;
56
using System.Text;
67

@@ -18,57 +19,46 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
1819
)
1920
.Where(static info => !info.Equals(default(PropertyFieldInfo)));
2021

22+
// Group fields by containing class
23+
var grouped = fieldsWithAttribute.Collect()
24+
.Select((list, _) => list
25+
.Where(info => info.ContainingClass is INamedTypeSymbol) // Filter to only INamedTypeSymbol
26+
.GroupBy(info => (INamedTypeSymbol)info.ContainingClass, SymbolEqualityComparer.Default)
27+
.Select(g => (ClassSymbol: g.Key, Fields: (IList<PropertyFieldInfo>)g.ToList()))
28+
.ToList()
29+
);
30+
2131
var compilationProvider = context.CompilationProvider;
22-
context.RegisterSourceOutput(fieldsWithAttribute.Combine(compilationProvider), (spc, tuple) =>
32+
33+
context.RegisterSourceOutput(grouped.Combine(compilationProvider), (spc, tuple) =>
2334
{
24-
var (info, compilation) = (tuple.Left, tuple.Right);
25-
GenerateProperty(spc, info, compilation);
35+
var (classGroups, compilation) = (tuple.Left, tuple.Right);
36+
foreach (var group in classGroups)
37+
{
38+
var classSymbol = group.ClassSymbol as INamedTypeSymbol;
39+
var fields = group.Fields;
40+
if (classSymbol != null)
41+
{
42+
GeneratePropertyClass(spc, classSymbol, fields, compilation);
43+
}
44+
}
2645
});
2746
}
2847

29-
private static void GenerateProperty(SourceProductionContext context, PropertyFieldInfo info, Compilation compilation)
48+
// New method to generate all properties for a class
49+
private static void GeneratePropertyClass(
50+
SourceProductionContext context,
51+
INamedTypeSymbol classSymbol,
52+
IList<PropertyFieldInfo> fields, // Use IList<T> for compatibility
53+
Compilation compilation)
3054
{
31-
var classSymbol = info.ContainingClass;
32-
var fieldSymbol = info.FieldSymbol;
33-
var fieldName = fieldSymbol.Name;
34-
var propertyName = PropertyGeneratorHelpers.ToPropertyName(fieldName);
35-
var typeName = fieldSymbol.Type.ToDisplayString();
36-
37-
// Rule 1: Class must be partial
38-
if (!PropertyGeneratorHelpers.IsPartial(classSymbol))
39-
{
40-
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Class '{classSymbol.Name}' must be partial to use [Property].");
41-
return;
42-
}
43-
44-
// Rule 2: Field must start with "_" followed by a letter, or a lowercase letter
45-
if (!PropertyGeneratorHelpers.IsValidFieldName(fieldName))
46-
{
47-
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Field '{fieldName}' must start with '_' followed by a letter, or a lowercase letter to use [Property].");
48-
return;
49-
}
50-
51-
// Rule 3: Property must not already exist
52-
if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName))
53-
{
54-
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Property '{propertyName}' already exists in '{classSymbol.Name}'.");
55-
return;
56-
}
57-
58-
// Attribute arguments
59-
var threadSafe = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
60-
var readOnly = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
61-
62-
// Check for ThreadObject
6355
var inheritsThreadObject = PropertyGeneratorHelpers.InheritsFrom(classSymbol, "ThunderDesign.Net.Threading.Objects.ThreadObject");
6456

6557
var source = new StringBuilder();
6658
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace ? null : classSymbol.ContainingNamespace.ToDisplayString();
6759

6860
if (!string.IsNullOrEmpty(ns))
69-
{
7061
source.AppendLine($"namespace {ns} {{");
71-
}
7262

7363
source.AppendLine("using ThunderDesign.Net.Threading.Extentions;");
7464
source.AppendLine("using ThunderDesign.Net.Threading.Objects;");
@@ -80,32 +70,43 @@ private static void GenerateProperty(SourceProductionContext context, PropertyFi
8070
if (!inheritsThreadObject && !PropertyGeneratorHelpers.FieldExists(classSymbol, "_Locker"))
8171
source.AppendLine(" protected readonly object _Locker = new object();");
8272

83-
// Add property
84-
var lockerArg = threadSafe ? "_Locker" : "null";
85-
if (readOnly)
73+
// Generate all properties
74+
foreach (var info in fields)
8675
{
87-
source.AppendLine($@"
76+
var fieldSymbol = info.FieldSymbol;
77+
var fieldName = fieldSymbol.Name;
78+
var propertyName = PropertyGeneratorHelpers.ToPropertyName(fieldName);
79+
var typeName = fieldSymbol.Type.ToDisplayString();
80+
81+
var readOnly = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
82+
var threadSafe = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
83+
84+
var lockerArg = threadSafe ? "_Locker" : "null";
85+
if (readOnly)
86+
{
87+
source.AppendLine($@"
8888
public {typeName} {propertyName}
8989
{{
9090
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
9191
}}");
92-
}
93-
else
94-
{
95-
source.AppendLine($@"
92+
}
93+
else
94+
{
95+
source.AppendLine($@"
9696
public {typeName} {propertyName}
9797
{{
9898
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
9999
set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}); }}
100100
}}");
101+
}
101102
}
102103

103104
source.AppendLine("}");
104105

105106
if (!string.IsNullOrEmpty(ns))
106107
source.AppendLine("}");
107108

108-
context.AddSource($"{classSymbol.Name}_{propertyName}_Property.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
109+
context.AddSource($"{classSymbol.Name}_Properties.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
109110
}
110111
}
111112

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace ThunderDesign.Net_PCL.Threading.Attributes
4+
{
5+
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = true)]
6+
public sealed class AlsoNotifyAttribute : Attribute
7+
{
8+
public string PropertyName { get; }
9+
10+
public AlsoNotifyAttribute(string propertyName)
11+
{
12+
PropertyName = propertyName;
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)