Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ObservableTaskCommandGenerator.cs
1using System;
3using System.Collections.Immutable;
4using System.Linq;
5using Microsoft.CodeAnalysis;
6using Microsoft.CodeAnalysis.CSharp.Syntax;
7
9{
10 [Generator(LanguageNames.CSharp)]
11 public class ObservableTaskCommandGenerator : IIncrementalGenerator
12 {
13 // Diagnostic for successfully generating a command.
14 private static readonly DiagnosticDescriptor debugGeneratedCommand = new DiagnosticDescriptor(
15 id: "NTSCG001",
16 title: "Command Generated",
17 messageFormat: "Generated command property '{0}' for method '{1}' in class '{2}'",
18 category: "ObservableTaskCommandGenerator",
19 defaultSeverity: DiagnosticSeverity.Info,
20 isEnabledByDefault: true);
21
22 private static readonly DiagnosticDescriptor unsupportedSignature = new DiagnosticDescriptor(
23 id: "NTSCG002",
24 title: "Unsupported Method Signature",
25 messageFormat: "Method '{0}' in class '{1}' has an unsupported signature for command generation",
26 category: "ObservableTaskCommandGenerator",
27 defaultSeverity: DiagnosticSeverity.Error,
28 isEnabledByDefault: true);
29
30 public void Initialize(IncrementalGeneratorInitializationContext context)
31 {
32 // Create a provider for all method declarations with any attribute.
33 IncrementalValuesProvider<MethodDeclarationSyntax> MethodDeclarations =
34 context.SyntaxProvider.CreateSyntaxProvider(
35 predicate: static (node, _) =>
36 node is MethodDeclarationSyntax M && M.AttributeLists.Count > 0,
37 transform: static (ctx, _) => (MethodDeclarationSyntax)ctx.Node)
38 .Where(method => method is not null);
39
40 // Combine the methods with the Compilation.
41 IncrementalValueProvider<(Compilation, ImmutableArray<MethodDeclarationSyntax>)> CompilationAndMethods =
42 context.CompilationProvider.Combine(MethodDeclarations.Collect());
43
44 // Register the source output.
45 context.RegisterSourceOutput(CompilationAndMethods, (spc, source) =>
46 {
47 (Compilation Compilation, ImmutableArray<MethodDeclarationSyntax> Methods) = source;
48 // Get the attribute symbol.
49 INamedTypeSymbol? AttributeSymbol = Compilation.GetTypeByMetadataName("NeuroAccessMaui.UI.MVVM.ObservableTaskCommandAttribute");
50 if (AttributeSymbol is null)
51 {
52 // If the attribute isn’t found, report an informational diagnostic.
53 spc.ReportDiagnostic(Diagnostic.Create(
54 new DiagnosticDescriptor(
55 id: "NTSCG000",
56 title: "Attribute Not Found",
57 messageFormat: "The attribute 'ObservableTaskCommandAttribute' was not found.",
58 category: "ObservableTaskCommandGenerator",
59 defaultSeverity: DiagnosticSeverity.Info,
60 isEnabledByDefault: true),
61 Location.None));
62 return;
63 }
64
65 foreach (MethodDeclarationSyntax MethodDeclaration in Methods)
66 {
67 SemanticModel SemanticModel = Compilation.GetSemanticModel(MethodDeclaration.SyntaxTree);
68 if (SemanticModel.GetDeclaredSymbol(MethodDeclaration) is not IMethodSymbol MethodSymbol)
69 continue;
70
71 // Check if the method is decorated with our attribute.
72 AttributeData? AttrData = MethodSymbol.GetAttributes()
73 .FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, AttributeSymbol));
74 if (AttrData is null)
75 continue;
76
77 // Get containing type info.
78 INamedTypeSymbol? ContainingType = MethodSymbol.ContainingType;
79 string NamespaceName = ContainingType.ContainingNamespace.ToDisplayString();
80 string ClassName = ContainingType.Name;
81 string MethodName = MethodSymbol.Name;
82
83 // Generate command property name (remove "Async" suffix if present).
84 string CommandName = MethodName.EndsWith("Async")
85 ? MethodName.Substring(0, MethodName.Length - 5) + "Command"
86 : MethodName + "Command";
87
88 // Determine the result type.
89 string ResultType;
90 bool ReturnsGenericTask = false;
91 if (MethodSymbol.ReturnType is INamedTypeSymbol NamedType)
92 {
93 if (NamedType.ConstructedFrom.ToDisplayString() == "System.Threading.Tasks.Task<TResult>")
94 {
95 ReturnsGenericTask = true;
96 ResultType = NamedType.TypeArguments[0].ToDisplayString();
97 }
98 else if (NamedType.ToDisplayString() == "System.Threading.Tasks.Task")
99 {
100 ResultType = "object";
101 }
102 else
103 {
104 // Not a Task type; skip generation.
105 spc.ReportDiagnostic(Diagnostic.Create(unsupportedSignature, MethodDeclaration.GetLocation(), MethodName, ClassName));
106 continue;
107 }
108 }
109 else
110 {
111 spc.ReportDiagnostic(Diagnostic.Create(unsupportedSignature, MethodDeclaration.GetLocation(), MethodName, ClassName));
112 continue;
113 }
114
115 // Determine the progress type.
116 // Now we support either a single parameter of type TaskContext<TProgress> or no parameters.
117 string ProgressType = "object"; // default fallback
118 if (MethodSymbol.Parameters.Length == 1)
119 {
120 IParameterSymbol P0 = MethodSymbol.Parameters[0];
121 if (P0.Type is INamedTypeSymbol ContextSymbol &&
122 ContextSymbol is { Name: "TaskContext", TypeArguments.Length: 1 })
123 {
124 ProgressType = ContextSymbol.TypeArguments[0].ToDisplayString();
125 }
126 else
127 {
128 spc.ReportDiagnostic(Diagnostic.Create(unsupportedSignature, MethodDeclaration.GetLocation(), MethodName, ClassName));
129 continue;
130 }
131 }
132 else if (MethodSymbol.Parameters.Length == 0)
133 {
134 // Parameterless method: we fall back to object.
135 ProgressType = "object";
136 }
137 else
138 {
139 spc.ReportDiagnostic(Diagnostic.Create(unsupportedSignature, MethodDeclaration.GetLocation(), MethodName, ClassName));
140 continue;
141 }
142
143 // Get the options value from the attribute.
144 // Default is "ObservableTaskCommandOptions.None".
145 string OptionsValue = "ObservableTaskCommandOptions.None";
146 if (AttrData.ConstructorArguments.Length > 0 && AttrData.ConstructorArguments[0].Value is int Value)
147 {
148 // We output the integer value as a cast to the enum.
149 OptionsValue = $"((ObservableTaskCommandOptions){Value})";
150 }
151 else
152 {
153 // Also check for named arguments.
154 foreach (KeyValuePair<string, TypedConstant> NamedArg in AttrData.NamedArguments)
155 {
156 if (NamedArg.Key == "Options" && NamedArg.Value.Value is int N)
157 {
158 OptionsValue = $"((ObservableTaskCommandOptions){N})";
159 break;
160 }
161 }
162 }
163
164 // Generate the lambda body.
165 string LambdaBody = "";
166 if (MethodSymbol.Parameters.Length == 0)
167 {
168 // Parameterless method.
169 LambdaBody = ReturnsGenericTask
170 ? $"await {MethodName}();"
171 : $"await {MethodName}();";
172 }
173 else if (MethodSymbol.Parameters.Length == 1)
174 {
175 // Method with TaskContext<TProgress> parameter.
176 LambdaBody = ReturnsGenericTask
177 ? $"await {MethodName}(context);"
178 : $"await {MethodName}(context);";
179 }
180 else
181 {
182 spc.ReportDiagnostic(Diagnostic.Create(unsupportedSignature, MethodDeclaration.GetLocation(), MethodName, ClassName));
183 continue;
184 }
185
186 // Build the generated source code using the extracted progress type and options.
187 string GeneratedSource = $@"
188using System;
189using System.Threading;
190using System.Threading.Tasks;
191using NeuroAccessMaui.UI.MVVM;
192
193namespace {NamespaceName}
194{{
195 public partial class {ClassName}
196 {{
197 private ObservableTaskCommand<{ProgressType}>? _{CommandName};
198 public ObservableTaskCommand<{ProgressType}> {CommandName} => _{CommandName} ??= new ObservableTaskCommand<{ProgressType}>(
199 async (context) =>
200 {{
201 {LambdaBody}
202 }},
203 null,
204 {OptionsValue});
205 }}
206}}
207";
208 // Add the generated source.
209 spc.AddSource($"{ClassName}_{CommandName}_generated.cs", GeneratedSource);
210 // Report diagnostic that command generation succeeded.
211 spc.ReportDiagnostic(Diagnostic.Create(debugGeneratedCommand, MethodDeclaration.GetLocation(), CommandName, MethodName, ClassName));
212 }
213 });
214 }
215 }
216}
Definition: ImplTypes.g.cs:58