Skip to content

Commit c87aac0

Browse files
CopilotYunchuWang
andcommitted
Add validation for C# identifier names in source generator
- Add DiagnosticDescriptors for invalid task and event names - Add IsValidCSharpIdentifier helper using SyntaxFacts.IsValidIdentifier - Update DurableTaskTypeInfo and DurableEventTypeInfo to track name locations - Report diagnostics for invalid identifiers and skip code generation - Add comprehensive tests for various invalid identifier scenarios - All 59 tests passing Co-authored-by: YunchuWang <[email protected]>
1 parent 70c741b commit c87aac0

File tree

2 files changed

+334
-6
lines changed

2 files changed

+334
-6
lines changed

src/Generators/DurableTaskSourceGenerator.cs

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,32 @@ public class DurableTaskSourceGenerator : IIncrementalGenerator
3939
* }
4040
*/
4141

42+
/// <summary>
43+
/// Diagnostic ID for invalid task names.
44+
/// </summary>
45+
const string InvalidTaskNameDiagnosticId = "DURABLE1001";
46+
47+
/// <summary>
48+
/// Diagnostic ID for invalid event names.
49+
/// </summary>
50+
const string InvalidEventNameDiagnosticId = "DURABLE1002";
51+
52+
static readonly DiagnosticDescriptor InvalidTaskNameRule = new(
53+
InvalidTaskNameDiagnosticId,
54+
title: "Invalid task name",
55+
messageFormat: "The task name '{0}' is not a valid C# identifier. Task names must start with a letter or underscore and contain only letters, digits, and underscores.",
56+
category: "DurableTask.Design",
57+
DiagnosticSeverity.Error,
58+
isEnabledByDefault: true);
59+
60+
static readonly DiagnosticDescriptor InvalidEventNameRule = new(
61+
InvalidEventNameDiagnosticId,
62+
title: "Invalid event name",
63+
messageFormat: "The event name '{0}' is not a valid C# identifier. Event names must start with a letter or underscore and contain only letters, digits, and underscores.",
64+
category: "DurableTask.Design",
65+
DiagnosticSeverity.Error,
66+
isEnabledByDefault: true);
67+
4268
/// <inheritdoc/>
4369
public void Initialize(IncrementalGeneratorInitializationContext context)
4470
{
@@ -166,13 +192,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
166192
ITypeSymbol? outputType = kind == DurableTaskKind.Entity ? null : taskType.TypeArguments.Last();
167193

168194
string taskName = classType.Name;
195+
Location? taskNameLocation = null;
169196
if (attribute.ArgumentList?.Arguments.Count > 0)
170197
{
171198
ExpressionSyntax expression = attribute.ArgumentList.Arguments[0].Expression;
172199
taskName = context.SemanticModel.GetConstantValue(expression).ToString();
200+
taskNameLocation = expression.GetLocation();
173201
}
174202

175-
return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind);
203+
return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind, taskNameLocation);
176204
}
177205

178206
static DurableEventTypeInfo? GetDurableEventTypeInfo(GeneratorSyntaxContext context)
@@ -204,6 +232,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
204232
}
205233

206234
string eventName = eventType.Name;
235+
Location? eventNameLocation = null;
207236

208237
if (attribute.ArgumentList?.Arguments.Count > 0)
209238
{
@@ -212,10 +241,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
212241
if (constantValue.HasValue && constantValue.Value is string value)
213242
{
214243
eventName = value;
244+
eventNameLocation = expression.GetLocation();
215245
}
216246
}
217247

218-
return new DurableEventTypeInfo(eventName, eventType);
248+
return new DurableEventTypeInfo(eventName, eventType, eventNameLocation);
219249
}
220250

221251
static DurableFunction? GetDurableFunction(GeneratorSyntaxContext context)
@@ -230,6 +260,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
230260
return null;
231261
}
232262

263+
/// <summary>
264+
/// Checks if a name is a valid C# identifier.
265+
/// </summary>
266+
/// <param name="name">The name to validate.</param>
267+
/// <returns>True if the name is a valid C# identifier, false otherwise.</returns>
268+
static bool IsValidCSharpIdentifier(string name)
269+
{
270+
if (string.IsNullOrEmpty(name))
271+
{
272+
return false;
273+
}
274+
275+
// Use Roslyn's built-in identifier validation
276+
return SyntaxFacts.IsValidIdentifier(name);
277+
}
278+
233279
static void Execute(
234280
SourceProductionContext context,
235281
Compilation compilation,
@@ -242,18 +288,47 @@ static void Execute(
242288
return;
243289
}
244290

291+
// Validate task names and report diagnostics for invalid identifiers
292+
foreach (DurableTaskTypeInfo task in allTasks)
293+
{
294+
if (!IsValidCSharpIdentifier(task.TaskName))
295+
{
296+
Location location = task.TaskNameLocation ?? Location.None;
297+
Diagnostic diagnostic = Diagnostic.Create(InvalidTaskNameRule, location, task.TaskName);
298+
context.ReportDiagnostic(diagnostic);
299+
}
300+
}
301+
302+
// Validate event names and report diagnostics for invalid identifiers
303+
foreach (DurableEventTypeInfo eventInfo in allEvents)
304+
{
305+
if (!IsValidCSharpIdentifier(eventInfo.EventName))
306+
{
307+
Location location = eventInfo.EventNameLocation ?? Location.None;
308+
Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
309+
context.ReportDiagnostic(diagnostic);
310+
}
311+
}
312+
245313
// This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific
246314
// code if we find the Durable Functions extension listed in the set of referenced assembly names.
247315
bool isDurableFunctions = compilation.ReferencedAssemblyNames.Any(
248316
assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
249317

250318
// Separate tasks into orchestrators, activities, and entities
319+
// Skip tasks with invalid names to avoid generating invalid code
251320
List<DurableTaskTypeInfo> orchestrators = new();
252321
List<DurableTaskTypeInfo> activities = new();
253322
List<DurableTaskTypeInfo> entities = new();
254323

255324
foreach (DurableTaskTypeInfo task in allTasks)
256325
{
326+
// Skip tasks with invalid names
327+
if (!IsValidCSharpIdentifier(task.TaskName))
328+
{
329+
continue;
330+
}
331+
257332
if (task.IsActivity)
258333
{
259334
activities.Add(task);
@@ -268,7 +343,17 @@ static void Execute(
268343
}
269344
}
270345

271-
int found = activities.Count + orchestrators.Count + entities.Count + allEvents.Length + allFunctions.Length;
346+
// Filter out events with invalid names
347+
List<DurableEventTypeInfo> validEvents = new();
348+
foreach (DurableEventTypeInfo eventInfo in allEvents)
349+
{
350+
if (IsValidCSharpIdentifier(eventInfo.EventName))
351+
{
352+
validEvents.Add(eventInfo);
353+
}
354+
}
355+
356+
int found = activities.Count + orchestrators.Count + entities.Count + validEvents.Count + allFunctions.Length;
272357
if (found == 0)
273358
{
274359
return;
@@ -347,7 +432,7 @@ public static class GeneratedDurableTaskExtensions
347432
}
348433

349434
// Generate WaitFor{EventName}Async methods for each event type
350-
foreach (DurableEventTypeInfo eventInfo in allEvents)
435+
foreach (DurableEventTypeInfo eventInfo in validEvents)
351436
{
352437
AddEventWaitMethod(sourceBuilder, eventInfo);
353438
AddEventSendMethod(sourceBuilder, eventInfo);
@@ -573,11 +658,13 @@ public DurableTaskTypeInfo(
573658
string taskName,
574659
ITypeSymbol? inputType,
575660
ITypeSymbol? outputType,
576-
DurableTaskKind kind)
661+
DurableTaskKind kind,
662+
Location? taskNameLocation = null)
577663
{
578664
this.TypeName = taskType;
579665
this.TaskName = taskName;
580666
this.Kind = kind;
667+
this.TaskNameLocation = taskNameLocation;
581668

582669
// Entities only have a state type parameter, not input/output
583670
if (kind == DurableTaskKind.Entity)
@@ -605,6 +692,7 @@ public DurableTaskTypeInfo(
605692
public string InputParameter { get; }
606693
public string OutputType { get; }
607694
public DurableTaskKind Kind { get; }
695+
public Location? TaskNameLocation { get; }
608696

609697
public bool IsActivity => this.Kind == DurableTaskKind.Activity;
610698

@@ -632,14 +720,16 @@ static string GetRenderedTypeExpression(ITypeSymbol? symbol)
632720

633721
class DurableEventTypeInfo
634722
{
635-
public DurableEventTypeInfo(string eventName, ITypeSymbol eventType)
723+
public DurableEventTypeInfo(string eventName, ITypeSymbol eventType, Location? eventNameLocation = null)
636724
{
637725
this.TypeName = GetRenderedTypeExpression(eventType);
638726
this.EventName = eventName;
727+
this.EventNameLocation = eventNameLocation;
639728
}
640729

641730
public string TypeName { get; }
642731
public string EventName { get; }
732+
public Location? EventNameLocation { get; }
643733

644734
static string GetRenderedTypeExpression(ITypeSymbol? symbol)
645735
{

0 commit comments

Comments
 (0)