Skip to content

Commit 2ec673f

Browse files
Property generated by Source Generator now chooses its Accessibility from the highest access from getter or setter
1 parent e42d14a commit 2ec673f

20 files changed

Lines changed: 99 additions & 67 deletions

README.md

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ThunderDesign.Net-PCL.Threading
1+
# ThunderDesign.Net-PCL.Threading
22
[![CI](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/actions/workflows/CI.yml/badge.svg)](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/actions/workflows/CI.yml)
33
[![CD](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/actions/workflows/CD.yml/badge.svg)](https://github.com/ThunderDesign/ThunderDesign.Net-PCL.Threading/actions/workflows/CD.yml)
44
[![Nuget](https://img.shields.io/nuget/v/ThunderDesign.Net-PCL.Threading)](https://www.nuget.org/packages/ThunderDesign.Net-PCL.Threading)
@@ -107,9 +107,10 @@ With the source generator, you only need to annotate your fields:
107107

108108
```csharp
109109
using ThunderDesign.Net.Threading.Attributes;
110+
110111
public partial class Person
111-
{
112-
[BindableProperty]
112+
{
113+
[BindableProperty]
113114
private string _name;
114115

115116
[Property]
@@ -128,7 +129,6 @@ using ThunderDesign.Net.Threading.Interfaces;
128129
public partial class Person : IBindableObject, INotifyPropertyChanged
129130
{
130131
public event PropertyChangedEventHandler PropertyChanged;
131-
132132
protected readonly object _Locker = new object();
133133

134134
public string Name
@@ -140,58 +140,70 @@ public partial class Person : IBindableObject, INotifyPropertyChanged
140140
public int Age
141141
{
142142
get { return this.GetProperty(ref _age, _Locker); }
143-
set { this.SetProperty(ref _age, value, _Locker, true); }
143+
set { this.SetProperty(ref _age, value, _Locker); }
144144
}
145145
}
146146
```
147147

148148
You can now use your `Person` class like this:
149149

150150
```csharp
151-
var person = new Person();
152-
person.Name = "Alice";
153-
person.Age = 30; // PropertyChanged event will be raised for Name changes if you subscribe to it.
151+
var person = new Person();
152+
person.Name = "Alice";
153+
person.Age = 30;
154+
// PropertyChanged event will be raised for Name changes if you subscribe to it.
154155
```
155156

156157
**No need to manually implement** property notification, thread safety, or boilerplate code—the generator does it for you!
157158

158-
> For more advanced scenarios, you can use attribute parameters to control property behavior (e.g., read-only, also notify other properties, or control accessor visibility).
159+
> For more advanced scenarios, you can use attribute parameters to control property behavior (e.g., read-only, also notify other properties, or control accessor/property visibility).
160+
161+
---
159162

160-
### Advanced: Customizing Property Accessors
163+
### Advanced: Customizing Getter and Setter Accessors
161164

162-
You can now control the visibility of generated property getters and setters using the `AccessorAccessibility` enum.
163-
This allows you to generate properties with `private`, `protected`, `internal`, or other C# accessibilities for the `get` and `set` accessors.
165+
You can control the visibility of the generated property's getter and setter using the `AccessorAccessibility` enum.
166+
The property itself will use the most accessible (widest) of the getter or setter's accessibilities.
164167

165168
#### Example
166169

167170
```csharp
168-
using ThunderDesign.Net.Threading.Attributes;
169-
using ThunderDesign.Net_PCL.Threading.Shared.Enums;
171+
using ThunderDesign.Net.Threading.Attributes;
172+
using ThunderDesign.Net.Threading.Enums;
170173

171174
public partial class Person
172175
{
173-
// Public getter, private setter [BindableProperty(getter: AccessorAccessibility.Public, setter: AccessorAccessibility.Private)] private string _name;
174-
// Internal getter, protected setter
176+
// Public getter, private setter (property will be public)
177+
[BindableProperty(getter: AccessorAccessibility.Public, setter: AccessorAccessibility.Private)]
178+
private string _name;
179+
180+
// Internal getter, protected setter (property will be internal)
175181
[Property(getter: AccessorAccessibility.Internal, setter: AccessorAccessibility.Protected)]
176182
private int _age;
177183
}
178184
```
179185

180186
**What gets generated:**
181-
182187
```csharp
183188
public partial class Person
184189
{
185-
public string Name { get { return this.GetProperty(ref _name, _Locker); } private set { this.SetProperty(ref _name, value, _Locker, true); } }
190+
public string Name
191+
{
192+
get { return this.GetProperty(ref _name, _Locker); }
193+
private set { this.SetProperty(ref _name, value, _Locker, true); }
194+
}
195+
186196
internal int Age
187197
{
188-
get { return this.GetProperty(ref _age, _Locker); }
198+
internal get { return this.GetProperty(ref _age, _Locker); }
189199
protected set { this.SetProperty(ref _age, value, _Locker); }
190200
}
191201
}
202+
192203
```
193204

194-
> The default for both getter and setter is `public` if not specified.
205+
> The property will be as accessible as its most accessible accessor (getter or setter).
206+
> The default for `getter`, and `setter` is `public` if not specified.
195207
196208
**Available options for `AccessorAccessibility`:**
197209
- `Public`
@@ -201,12 +213,13 @@ public partial class Person
201213
- `ProtectedInternal`
202214
- `PrivateProtected`
203215

216+
---
217+
204218
### Advanced: Notify Other Properties
205219

206-
You can now notify other properties when a specific property changes by using the `alsoNotify` parameter in the `[BindableProperty]` attribute.
220+
You can notify other properties when a specific property changes by using the `alsoNotify` parameter in the `[BindableProperty]` attribute.
207221

208222
#### Example
209-
210223
```csharp
211224
using ThunderDesign.Net.Threading.Attributes;
212225

@@ -228,7 +241,6 @@ public partial class Person
228241
public partial class Person : IBindableObject, INotifyPropertyChanged
229242
{
230243
public event PropertyChangedEventHandler PropertyChanged;
231-
232244
protected readonly object _Locker = new object();
233245

234246
public string FirstName
@@ -248,23 +260,20 @@ public partial class Person : IBindableObject, INotifyPropertyChanged
248260
get { return this.GetProperty(ref _lastName, _Locker); }
249261
set
250262
{
251-
this.SetProperty(ref _lastName, value, _Locker, true);
252-
OnPropertyChanged(nameof(DisplayName));
263+
if (this.SetProperty(ref _lastName, value, _Locker, true))
264+
{
265+
this.OnPropertyChanged(nameof(DisplayName));
266+
}
253267
}
254268
}
255269

256270
public string DisplayName => $"{FirstName} {LastName}";
257-
258-
protected void OnPropertyChanged(string propertyName)
259-
{
260-
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
261-
}
262271
}
263272
```
264273

265274
> This feature is particularly useful for computed properties like `DisplayName` that depend on other properties.
266275
267-
----
276+
---
268277

269278
## Installation
270279

@@ -302,4 +311,6 @@ This can be overwritten durring creation or by setting Property `WaitOnNotifyCol
302311
Observable Objects Property `WaitOnNotifyPropertyChanged` has been renamed to Property `WaitOnNotifying`.
303312

304313
Observable Collections Property `WaitOnNotifyCollectionChanged` has been removed and now uses Property `WaitOnNotifying`.
305-
----
314+
----
315+
316+

src/ThunderDesign.Net-PCL.SourceGenerators/Helpers/PropertyGeneratorHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Microsoft.CodeAnalysis.CSharp.Syntax;
44
using System.Linq;
55

6-
namespace ThunderDesign.Net_PCL.SourceGenerators.Helpers
6+
namespace ThunderDesign.Net.SourceGenerators.Helpers
77
{
88
internal static class PropertyGeneratorHelpers
99
{

src/ThunderDesign.Net-PCL.SourceGenerators/ThunderDesign.Net-PCL.SourceGenerators.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFramework>netstandard2.0</TargetFramework>
55
<LangVersion>latest</LangVersion>
66
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
7-
<RootNamespace>ThunderDesign.Net_PCL.SourceGenerators</RootNamespace>
7+
<RootNamespace>ThunderDesign.Net.SourceGenerators</RootNamespace>
88
<IncludeBuildOutput>false</IncludeBuildOutput>
99
<IncludeAnalyzer>true</IncludeAnalyzer>
1010
<!-- NuGet package metadata -->

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
using System.Diagnostics;
77
using System.Linq;
88
using System.Text;
9-
using ThunderDesign.Net_PCL.SourceGenerators.Helpers;
9+
using ThunderDesign.Net.SourceGenerators.Helpers;
1010

11-
namespace ThunderDesign.Net_PCL.SourceGenerators
11+
namespace ThunderDesign.Net.SourceGenerators
1212
{
1313
[Generator]
1414
public class UnifiedPropertyGenerator : IIncrementalGenerator
@@ -201,7 +201,7 @@ public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMem
201201
}");
202202
}
203203

204-
// Helper to convert AccessorAccessibility to C# keyword
204+
// Helper to convert AccessorAccessibility to C# keyword (empty string means public)
205205
static string ToAccessorString(object accessor)
206206
{
207207
var value = accessor?.ToString() ?? "Public";
@@ -217,6 +217,29 @@ static string ToAccessorString(object accessor)
217217
};
218218
}
219219

220+
// Helper to rank accessibilities for comparison
221+
static int GetAccessibilityRank(string access)
222+
{
223+
return access switch
224+
{
225+
"Public" => 6,
226+
"ProtectedInternal" => 5,
227+
"Internal" => 4,
228+
"Protected" => 3,
229+
"PrivateProtected" => 2,
230+
"Private" => 1,
231+
_ => 0
232+
};
233+
}
234+
235+
// Helper to get the widest (most accessible) accessibility
236+
static string GetWidestAccessibility(object getter, object setter)
237+
{
238+
string getterStr = getter?.ToString() ?? "Public";
239+
string setterStr = setter?.ToString() ?? "Public";
240+
return GetAccessibilityRank(getterStr) >= GetAccessibilityRank(setterStr) ? getterStr : setterStr;
241+
}
242+
220243
// Generate all bindable properties
221244
foreach (var info in bindableFields)
222245
{
@@ -251,19 +274,19 @@ static string ToAccessorString(object accessor)
251274
if (alsoNotify == null)
252275
alsoNotify = new string[0];
253276

254-
// Default to Public if not present
255277
var getter = args.Length > 4 ? args[4].Value : null;
256278
var setter = args.Length > 5 ? args[5].Value : null;
257279

258280
var getterStr = ToAccessorString(getter);
259281
var setterStr = ToAccessorString(setter);
282+
var propertyAccessibilityStr = ToAccessorString(GetWidestAccessibility(getter, setter));
260283

261284
var lockerArg = threadSafe ? "_Locker" : "null";
262285
var notifyArg = notify ? "true" : "false";
263286
if (readOnly)
264287
{
265288
source.AppendLine($@"
266-
public {typeName} {propertyName}
289+
{propertyAccessibilityStr}{typeName} {propertyName}
267290
{{
268291
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
269292
}}");
@@ -294,7 +317,7 @@ static string ToAccessorString(object accessor)
294317
}
295318

296319
source.AppendLine($@"
297-
public {typeName} {propertyName}
320+
{propertyAccessibilityStr}{typeName} {propertyName}
298321
{{
299322
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
300323
{setAccessor}
@@ -305,7 +328,6 @@ static string ToAccessorString(object accessor)
305328
// Generate all regular properties
306329
foreach (var info in propertyFields)
307330
{
308-
// Skip if any rule failed (diagnostic already reported)
309331
var propertyName = PropertyGeneratorHelpers.ToPropertyName(info.FieldSymbol.Name);
310332
if (!PropertyGeneratorHelpers.IsPartial(classSymbol) ||
311333
!PropertyGeneratorHelpers.IsValidFieldName(info.FieldSymbol.Name) ||
@@ -326,20 +348,21 @@ static string ToAccessorString(object accessor)
326348

327349
var getterStr = ToAccessorString(getter);
328350
var setterStr = ToAccessorString(setter);
351+
var propertyAccessibilityStr = ToAccessorString(GetWidestAccessibility(getter, setter));
329352

330353
var lockerArg = threadSafe ? "_Locker" : "null";
331354
if (readOnly)
332355
{
333356
source.AppendLine($@"
334-
public {typeName} {propertyName}
357+
{propertyAccessibilityStr}{typeName} {propertyName}
335358
{{
336359
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
337360
}}");
338361
}
339362
else
340363
{
341364
source.AppendLine($@"
342-
public {typeName} {propertyName}
365+
{propertyAccessibilityStr}{typeName} {propertyName}
343366
{{
344367
{getterStr}get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
345368
{setterStr}set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}); }}

src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/BindablePropertyAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using ThunderDesign.Net.Threading.Enums;
33

4-
namespace ThunderDesign.Net_PCL.Threading.Attributes
4+
namespace ThunderDesign.Net.Threading.Attributes
55
{
66
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
77
public sealed class BindablePropertyAttribute : Attribute

src/ThunderDesign.Net-PCL.Threading.Shared/Attributes/PropertyAttribute.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
42
using ThunderDesign.Net.Threading.Enums;
53

6-
namespace ThunderDesign.Net_PCL.Threading.Attributes
4+
namespace ThunderDesign.Net.Threading.Attributes
75
{
86
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
97
public sealed class PropertyAttribute : Attribute

src/ThunderDesign.Net-PCL.Threading.Shared/ThunderDesign.Net-PCL.Threading.Shared.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>netstandard1.0;netstandard1.3;netstandard2.0;net461;net6.0;net8.0</TargetFrameworks>
5-
<RootNamespace>ThunderDesign.Net_PCL.Threading.Shared</RootNamespace>
5+
<RootNamespace>ThunderDesign.Net.Threading.Shared</RootNamespace>
66
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
77
<IncludeBuildOutput>true</IncludeBuildOutput>
88
<IncludeContentInPack>true</IncludeContentInPack>

src/ThunderDesign.Net-PCL.Threading/Collections/HashSetThreadSafe.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Threading;
4-
using ThunderDesign.Net_PCL.Threading.Interfaces;
4+
using ThunderDesign.Net.Threading.Interfaces;
55

6-
namespace ThunderDesign.Net_PCL.Threading.Collections
6+
namespace ThunderDesign.Net.Threading.Collections
77
{
88
#if NETSTANDARD1_3_OR_GREATER || NET6_0_OR_GREATER
99
public class HashSetThreadSafe<T> : HashSet<T>, IHashSetThreadSafe<T>

src/ThunderDesign.Net-PCL.Threading/Collections/LinkedListThreadSafe.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System.Collections.Generic;
22
using System.Threading;
3-
using ThunderDesign.Net_PCL.Threading.Interfaces;
3+
using ThunderDesign.Net.Threading.Interfaces;
44

5-
namespace ThunderDesign.Net_PCL.Threading.Collections
5+
namespace ThunderDesign.Net.Threading.Collections
66
{
77
#if NETSTANDARD1_3_OR_GREATER || NET6_0_OR_GREATER
88
public class LinkedListThreadSafe<T> : LinkedList<T>, ILinkedListThreadSafe<T>

src/ThunderDesign.Net-PCL.Threading/Collections/ListThreadSafe.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
using System.Collections.Generic;
33
using System.Collections.ObjectModel;
44
using System.Threading;
5-
using ThunderDesign.Net_PCL.Threading.Interfaces;
5+
using ThunderDesign.Net.Threading.Interfaces;
66

7-
namespace ThunderDesign.Net_PCL.Threading.Collections
7+
namespace ThunderDesign.Net.Threading.Collections
88
{
99
public class ListThreadSafe<T> : List<T>, IListThreadSafe<T>
1010
{

0 commit comments

Comments
 (0)