Skip to content

Commit b9bb0b0

Browse files
Added Source Generator for creating Properties from Fields
1 parent 58b2814 commit b9bb0b0

File tree

12 files changed

+527
-4
lines changed

12 files changed

+527
-4
lines changed

samples/Xamarin/SimpleBinding/SimpleBinding.Android/Resources/Resource.designer.cs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/Xamarin/SimpleContacts/SimpleContacts.Android/Resources/Resource.designer.cs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/Xamarin/SimpleContacts/SimpleContacts.sln

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ EndProject
88
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleContacts.iOS", "SimpleContacts.iOS\SimpleContacts.iOS.csproj", "{C1F8D9DD-88FB-42E3-A3A1-01F2554FFD1B}"
99
EndProject
1010
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleContacts", "SimpleContacts\SimpleContacts.csproj", "{42D29358-CA82-4C55-9B3D-D8AB994C0FDC}"
11+
ProjectSection(ProjectDependencies) = postProject
12+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2} = {C0A3769B-4580-2360-A1DF-C2A16B56DBF2}
13+
EndProjectSection
1114
EndProject
1215
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThunderDesign.Net-PCL.Threading", "..\..\..\src\ThunderDesign.Net-PCL.Threading\ThunderDesign.Net-PCL.Threading.csproj", "{8616AAA2-E21C-4D85-BE38-4124A58ECA97}"
1316
EndProject
17+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThunderDesign.Net-PCL.SourceGenerators", "..\..\..\src\ThunderDesign.Net-PCL.SourceGenerators\ThunderDesign.Net-PCL.SourceGenerators.csproj", "{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}"
18+
EndProject
1419
Global
1520
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1621
Debug|Any CPU = Debug|Any CPU
@@ -87,6 +92,18 @@ Global
8792
{8616AAA2-E21C-4D85-BE38-4124A58ECA97}.Release|iPhone.Build.0 = Release|Any CPU
8893
{8616AAA2-E21C-4D85-BE38-4124A58ECA97}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
8994
{8616AAA2-E21C-4D85-BE38-4124A58ECA97}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
95+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
96+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
97+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Debug|iPhone.ActiveCfg = Debug|Any CPU
98+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Debug|iPhone.Build.0 = Debug|Any CPU
99+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
100+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
101+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
102+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Release|Any CPU.Build.0 = Release|Any CPU
103+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Release|iPhone.ActiveCfg = Release|Any CPU
104+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Release|iPhone.Build.0 = Release|Any CPU
105+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
106+
{C0A3769B-4580-2360-A1DF-C2A16B56DBF2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
90107
EndGlobalSection
91108
GlobalSection(SolutionProperties) = preSolution
92109
HideSolutionNode = FALSE

samples/Xamarin/SimpleContacts/SimpleContacts/SimpleContacts.csproj

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
55
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
6+
<LangVersion>9.0</LangVersion>
67
</PropertyGroup>
78

89
<ItemGroup>
@@ -25,4 +26,12 @@
2526
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
2627
</EmbeddedResource>
2728
</ItemGroup>
29+
30+
<ItemGroup>
31+
<None Include="$(TargetPath)" Pack="true" PackagePath="analyzers/dotnet/cs/" />
32+
</ItemGroup>
33+
34+
<ItemGroup>
35+
<Analyzer Include="..\..\..\..\src\ThunderDesign.Net-PCL.SourceGenerators\bin\Debug\netstandard2.0\ThunderDesign.Net-PCL.SourceGenerators.dll" />
36+
</ItemGroup>
2837
</Project>
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
namespace ThunderDesign.Net_PCL.SourceGenerators
11+
{
12+
[Generator]
13+
public class BindablePropertyGenerator : IIncrementalGenerator
14+
{
15+
public void Initialize(IncrementalGeneratorInitializationContext context)
16+
{
17+
var fieldsWithAttribute = context.SyntaxProvider
18+
.CreateSyntaxProvider(
19+
predicate: static (node, _) => node is FieldDeclarationSyntax fds && fds.AttributeLists.Count > 0,
20+
transform: static (ctx, _) => GetBindableField(ctx)
21+
)
22+
.Where(static info => !info.Equals(default(BindableFieldInfo)));
23+
24+
context.RegisterSourceOutput(fieldsWithAttribute, (spc, info) =>
25+
{
26+
GenerateBindableProperty(spc, info);
27+
});
28+
}
29+
30+
private static BindableFieldInfo GetBindableField(GeneratorSyntaxContext context)
31+
{
32+
var fieldDecl = (FieldDeclarationSyntax)context.Node;
33+
var semanticModel = context.SemanticModel;
34+
35+
foreach (var variable in fieldDecl.Declaration.Variables)
36+
{
37+
var symbol = semanticModel.GetDeclaredSymbol(variable);
38+
if (symbol is IFieldSymbol fieldSymbol)
39+
{
40+
foreach (var attr in fieldSymbol.GetAttributes())
41+
{
42+
if (attr.AttributeClass?.Name == "BindablePropertyAttribute")
43+
{
44+
var containingClass = fieldSymbol.ContainingType;
45+
return new BindableFieldInfo
46+
{
47+
FieldSymbol = fieldSymbol,
48+
ContainingClass = containingClass,
49+
AttributeData = attr,
50+
FieldDeclaration = fieldDecl
51+
};
52+
}
53+
}
54+
}
55+
}
56+
return default(BindableFieldInfo);
57+
}
58+
59+
private static void GenerateBindableProperty(SourceProductionContext context, BindableFieldInfo info)
60+
{
61+
var classSymbol = info.ContainingClass;
62+
var fieldSymbol = info.FieldSymbol;
63+
var fieldName = fieldSymbol.Name;
64+
var propertyName = PropertyGeneratorHelpers.ToPropertyName(fieldName);
65+
var typeName = fieldSymbol.Type.ToDisplayString();
66+
67+
// Rule 1: Class must be partial
68+
if (!PropertyGeneratorHelpers.IsPartial(classSymbol))
69+
{
70+
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Class '{classSymbol.Name}' must be partial to use [BindableProperty].");
71+
return;
72+
}
73+
74+
// Rule 2: Field must start with "_" or lowercase
75+
if (!PropertyGeneratorHelpers.IsValidFieldName(fieldName))
76+
{
77+
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Field '{fieldName}' must start with '_' or a lowercase letter to use [BindableProperty].");
78+
return;
79+
}
80+
81+
// Rule 3: Property must not already exist
82+
if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName))
83+
{
84+
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Property '{propertyName}' already exists in '{classSymbol.Name}'.");
85+
return;
86+
}
87+
88+
// Attribute arguments
89+
var threadSafe = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
90+
var notify = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
91+
var readOnly = info.AttributeData.ConstructorArguments.Length > 2 && (bool)info.AttributeData.ConstructorArguments[2].Value!;
92+
93+
// Check for INotifyPropertyChanged, IBindableObject, ThreadObject
94+
var implementsINotify = ImplementsInterface(classSymbol, "System.ComponentModel.INotifyPropertyChanged");
95+
var implementsIBindable = ImplementsInterface(classSymbol, "ThunderDesign.Net.Threading.Interfaces.IBindableObject");
96+
97+
// Check for ThreadObject
98+
var inheritsThreadObject = PropertyGeneratorHelpers.InheritsFrom(classSymbol, "ThunderDesign.Net.Threading.Objects.ThreadObject");
99+
100+
var source = new StringBuilder();
101+
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace ? null : classSymbol.ContainingNamespace.ToDisplayString();
102+
103+
if (!string.IsNullOrEmpty(ns))
104+
{
105+
source.AppendLine($"namespace {ns} {{");
106+
}
107+
108+
source.AppendLine("using ThunderDesign.Net.Threading.Extentions;");
109+
source.AppendLine("using ThunderDesign.Net.Threading.Interfaces;");
110+
source.AppendLine("using ThunderDesign.Net.Threading.Objects;");
111+
112+
source.AppendLine($"partial class {classSymbol.Name}");
113+
114+
// Add interface if needed
115+
var interfaces = new List<string>();
116+
if (!implementsIBindable)
117+
interfaces.Add("IBindableObject");
118+
if (interfaces.Count > 0)
119+
source.Append(" : " + string.Join(", ", interfaces));
120+
source.AppendLine();
121+
source.AppendLine("{");
122+
123+
// Add event if needed
124+
if (!implementsINotify)
125+
source.AppendLine(" public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
126+
127+
// Add _Locker if needed
128+
if (!inheritsThreadObject)
129+
source.AppendLine(" protected readonly object _Locker = new object();");
130+
131+
// Add OnPropertyChanged if needed
132+
if (!implementsIBindable)
133+
{
134+
source.AppendLine(@"
135+
public virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = """")
136+
{
137+
this.NotifyPropertyChanged(PropertyChanged, propertyName);
138+
}");
139+
}
140+
141+
// Add property
142+
var lockerArg = threadSafe ? "_Locker" : "null";
143+
var notifyArg = notify ? "true" : "false";
144+
if (readOnly)
145+
{
146+
source.AppendLine($@"
147+
public {typeName} {propertyName}
148+
{{
149+
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
150+
}}");
151+
}
152+
else
153+
{
154+
source.AppendLine($@"
155+
public {typeName} {propertyName}
156+
{{
157+
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
158+
set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}, {notifyArg}); }}
159+
}}");
160+
}
161+
162+
source.AppendLine("}");
163+
164+
if (!string.IsNullOrEmpty(ns))
165+
source.AppendLine("}");
166+
167+
context.AddSource($"{classSymbol.Name}_{propertyName}_BindableProperty.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
168+
}
169+
170+
private static bool ImplementsInterface(INamedTypeSymbol type, string interfaceName)
171+
{
172+
return type.AllInterfaces.Any(i => i.ToDisplayString() == interfaceName);
173+
}
174+
175+
private struct BindableFieldInfo
176+
{
177+
public IFieldSymbol FieldSymbol { get; set; }
178+
public INamedTypeSymbol ContainingClass { get; set; }
179+
public AttributeData AttributeData { get; set; }
180+
public FieldDeclarationSyntax FieldDeclaration { get; set; }
181+
}
182+
}
183+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Text;
9+
using ThunderDesign.Net.Threading.Extentions;
10+
11+
namespace ThunderDesign.Net_PCL.SourceGenerators
12+
{
13+
[Generator]
14+
public class PropertyGenerator : IIncrementalGenerator
15+
{
16+
public void Initialize(IncrementalGeneratorInitializationContext context)
17+
{
18+
var fieldsWithAttribute = context.SyntaxProvider
19+
.CreateSyntaxProvider(
20+
predicate: static (node, _) => node is FieldDeclarationSyntax fds && fds.AttributeLists.Count > 0,
21+
transform: static (ctx, _) => PropertyGeneratorHelpers.GetFieldWithAttribute(ctx, "PropertyAttribute")
22+
)
23+
.Where(static info => !info.Equals(default(PropertyFieldInfo)));
24+
25+
context.RegisterSourceOutput(fieldsWithAttribute, (spc, info) =>
26+
{
27+
GenerateProperty(spc, info);
28+
});
29+
}
30+
31+
private static void GenerateProperty(SourceProductionContext context, PropertyFieldInfo info)
32+
{
33+
var classSymbol = info.ContainingClass;
34+
var fieldSymbol = info.FieldSymbol;
35+
var fieldName = fieldSymbol.Name;
36+
var propertyName = PropertyGeneratorHelpers.ToPropertyName(fieldName);
37+
var typeName = fieldSymbol.Type.ToDisplayString();
38+
39+
// Rule 1: Class must be partial
40+
if (!PropertyGeneratorHelpers.IsPartial(classSymbol))
41+
{
42+
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Class '{classSymbol.Name}' must be partial to use [Property].");
43+
return;
44+
}
45+
46+
// Rule 2: Field must start with "_" or lowercase
47+
if (!PropertyGeneratorHelpers.IsValidFieldName(fieldName))
48+
{
49+
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Field '{fieldName}' must start with '_' or a lowercase letter to use [Property].");
50+
return;
51+
}
52+
53+
// Rule 3: Property must not already exist
54+
if (PropertyGeneratorHelpers.PropertyExists(classSymbol, propertyName))
55+
{
56+
PropertyGeneratorHelpers.ReportDiagnostic(context, info.FieldDeclaration.GetLocation(), $"Property '{propertyName}' already exists in '{classSymbol.Name}'.");
57+
return;
58+
}
59+
60+
// Attribute arguments
61+
var threadSafe = info.AttributeData.ConstructorArguments.Length > 0 && (bool)info.AttributeData.ConstructorArguments[0].Value!;
62+
var readOnly = info.AttributeData.ConstructorArguments.Length > 1 && (bool)info.AttributeData.ConstructorArguments[1].Value!;
63+
64+
// Check for ThreadObject
65+
var inheritsThreadObject = PropertyGeneratorHelpers.InheritsFrom(classSymbol, "ThunderDesign.Net.Threading.Objects.ThreadObject");
66+
67+
var source = new StringBuilder();
68+
var ns = classSymbol.ContainingNamespace.IsGlobalNamespace ? null : classSymbol.ContainingNamespace.ToDisplayString();
69+
70+
if (!string.IsNullOrEmpty(ns))
71+
{
72+
source.AppendLine($"namespace {ns} {{");
73+
}
74+
75+
source.AppendLine("using ThunderDesign.Net.Threading.Extentions;");
76+
source.AppendLine("using ThunderDesign.Net.Threading.Objects;");
77+
78+
source.AppendLine($"partial class {classSymbol.Name}");
79+
source.AppendLine("{");
80+
81+
// Add _Locker if needed
82+
if (!inheritsThreadObject)
83+
source.AppendLine(" protected readonly object _Locker = new object();");
84+
85+
// Add property
86+
var lockerArg = threadSafe ? "_Locker" : "null";
87+
if (readOnly)
88+
{
89+
source.AppendLine($@"
90+
public {typeName} {propertyName}
91+
{{
92+
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
93+
}}");
94+
}
95+
else
96+
{
97+
source.AppendLine($@"
98+
public {typeName} {propertyName}
99+
{{
100+
get {{ return this.GetProperty(ref {fieldName}, {lockerArg}); }}
101+
set {{ this.SetProperty(ref {fieldName}, value, {lockerArg}); }}
102+
}}");
103+
}
104+
105+
source.AppendLine("}");
106+
107+
if (!string.IsNullOrEmpty(ns))
108+
source.AppendLine("}");
109+
110+
context.AddSource($"{classSymbol.Name}_{propertyName}_Property.g.cs", SourceText.From(source.ToString(), Encoding.UTF8));
111+
}
112+
}
113+
114+
public struct PropertyFieldInfo
115+
{
116+
public IFieldSymbol FieldSymbol { get; set; }
117+
public INamedTypeSymbol ContainingClass { get; set; }
118+
public AttributeData AttributeData { get; set; }
119+
public FieldDeclarationSyntax FieldDeclaration { get; set; }
120+
}
121+
}

0 commit comments

Comments
 (0)