-
Notifications
You must be signed in to change notification settings - Fork 53
Validate C# identifiers in DurableTask source generator #578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
70c741b
c87aac0
7055e80
0b4531f
c83ab73
59e8011
dc69325
4dce208
392ca5e
ebd73f9
d093fc5
62d3af4
bf3d673
1d5e901
5e99ac1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| using System.Collections.Immutable; | ||
| using System.Diagnostics; | ||
| using System.Text; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
|
@@ -39,6 +40,32 @@ | |
| * } | ||
| */ | ||
|
|
||
| /// <summary> | ||
| /// Diagnostic ID for invalid task names. | ||
| /// </summary> | ||
| const string InvalidTaskNameDiagnosticId = "DURABLE1001"; | ||
|
|
||
| /// <summary> | ||
| /// Diagnostic ID for invalid event names. | ||
| /// </summary> | ||
| const string InvalidEventNameDiagnosticId = "DURABLE1002"; | ||
|
|
||
| static readonly DiagnosticDescriptor InvalidTaskNameRule = new( | ||
| InvalidTaskNameDiagnosticId, | ||
|
Check warning on line 54 in src/Generators/DurableTaskSourceGenerator.cs
|
||
| title: "Invalid task name", | ||
| 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.", | ||
| category: "DurableTask.Design", | ||
| DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true); | ||
|
|
||
| static readonly DiagnosticDescriptor InvalidEventNameRule = new( | ||
| InvalidEventNameDiagnosticId, | ||
|
Check warning on line 62 in src/Generators/DurableTaskSourceGenerator.cs
|
||
| title: "Invalid event name", | ||
| 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.", | ||
| category: "DurableTask.Design", | ||
| DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true); | ||
|
|
||
| /// <inheritdoc/> | ||
| public void Initialize(IncrementalGeneratorInitializationContext context) | ||
| { | ||
|
|
@@ -166,13 +193,15 @@ | |
| ITypeSymbol? outputType = kind == DurableTaskKind.Entity ? null : taskType.TypeArguments.Last(); | ||
|
|
||
| string taskName = classType.Name; | ||
| Location? taskNameLocation = null; | ||
| if (attribute.ArgumentList?.Arguments.Count > 0) | ||
| { | ||
| ExpressionSyntax expression = attribute.ArgumentList.Arguments[0].Expression; | ||
| taskName = context.SemanticModel.GetConstantValue(expression).ToString(); | ||
| taskNameLocation = expression.GetLocation(); | ||
| } | ||
|
|
||
| return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind); | ||
| return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind, taskNameLocation); | ||
| } | ||
|
|
||
| static DurableEventTypeInfo? GetDurableEventTypeInfo(GeneratorSyntaxContext context) | ||
|
|
@@ -204,6 +233,7 @@ | |
| } | ||
|
|
||
| string eventName = eventType.Name; | ||
| Location? eventNameLocation = null; | ||
|
|
||
| if (attribute.ArgumentList?.Arguments.Count > 0) | ||
| { | ||
|
|
@@ -212,10 +242,11 @@ | |
| if (constantValue.HasValue && constantValue.Value is string value) | ||
| { | ||
| eventName = value; | ||
| eventNameLocation = expression.GetLocation(); | ||
| } | ||
| } | ||
|
|
||
| return new DurableEventTypeInfo(eventName, eventType); | ||
| return new DurableEventTypeInfo(eventName, eventType, eventNameLocation); | ||
| } | ||
|
|
||
| static DurableFunction? GetDurableFunction(GeneratorSyntaxContext context) | ||
|
|
@@ -230,6 +261,22 @@ | |
| return null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks if a name is a valid C# identifier. | ||
| /// </summary> | ||
| /// <param name="name">The name to validate.</param> | ||
| /// <returns>True if the name is a valid C# identifier, false otherwise.</returns> | ||
| static bool IsValidCSharpIdentifier(string name) | ||
| { | ||
| if (string.IsNullOrEmpty(name)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Use Roslyn's built-in identifier validation | ||
| return SyntaxFacts.IsValidIdentifier(name); | ||
| } | ||
|
|
||
| static void Execute( | ||
| SourceProductionContext context, | ||
| Compilation compilation, | ||
|
|
@@ -242,18 +289,47 @@ | |
| return; | ||
| } | ||
|
|
||
| // Validate task names and report diagnostics for invalid identifiers | ||
| foreach (DurableTaskTypeInfo task in allTasks) | ||
| { | ||
| if (!IsValidCSharpIdentifier(task.TaskName)) | ||
| { | ||
| Location location = task.TaskNameLocation ?? Location.None; | ||
| Diagnostic diagnostic = Diagnostic.Create(InvalidTaskNameRule, location, task.TaskName); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
|
||
|
|
||
| // Validate event names and report diagnostics for invalid identifiers | ||
| foreach (DurableEventTypeInfo eventInfo in allEvents) | ||
| { | ||
| if (!IsValidCSharpIdentifier(eventInfo.EventName)) | ||
| { | ||
| Location location = eventInfo.EventNameLocation ?? Location.None; | ||
| Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
YunchuWang marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
YunchuWang marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
|
||
|
|
||
| // This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific | ||
| // code if we find the Durable Functions extension listed in the set of referenced assembly names. | ||
| bool isDurableFunctions = compilation.ReferencedAssemblyNames.Any( | ||
| assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase)); | ||
|
|
||
| // Separate tasks into orchestrators, activities, and entities | ||
| // Skip tasks with invalid names to avoid generating invalid code | ||
| List<DurableTaskTypeInfo> orchestrators = new(); | ||
| List<DurableTaskTypeInfo> activities = new(); | ||
| List<DurableTaskTypeInfo> entities = new(); | ||
|
|
||
| foreach (DurableTaskTypeInfo task in allTasks) | ||
| { | ||
| // Skip tasks with invalid names | ||
| if (!IsValidCSharpIdentifier(task.TaskName)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (task.IsActivity) | ||
| { | ||
| activities.Add(task); | ||
|
|
@@ -268,7 +344,12 @@ | |
| } | ||
| } | ||
|
|
||
| int found = activities.Count + orchestrators.Count + entities.Count + allEvents.Length + allFunctions.Length; | ||
| // Filter out events with invalid names | ||
| List<DurableEventTypeInfo> validEvents = allEvents | ||
| .Where(eventInfo => IsValidCSharpIdentifier(eventInfo.EventName)) | ||
| .ToList(); | ||
|
|
||
| int found = activities.Count + orchestrators.Count + entities.Count + validEvents.Count + allFunctions.Length; | ||
| if (found == 0) | ||
| { | ||
| return; | ||
|
|
@@ -347,7 +428,7 @@ | |
| } | ||
|
|
||
| // Generate WaitFor{EventName}Async methods for each event type | ||
| foreach (DurableEventTypeInfo eventInfo in allEvents) | ||
| foreach (DurableEventTypeInfo eventInfo in validEvents) | ||
| { | ||
| AddEventWaitMethod(sourceBuilder, eventInfo); | ||
| AddEventSendMethod(sourceBuilder, eventInfo); | ||
|
|
@@ -588,11 +669,13 @@ | |
| string taskName, | ||
| ITypeSymbol? inputType, | ||
| ITypeSymbol? outputType, | ||
| DurableTaskKind kind) | ||
| DurableTaskKind kind, | ||
| Location? taskNameLocation = null) | ||
| { | ||
| this.TypeName = taskType; | ||
| this.TaskName = taskName; | ||
| this.Kind = kind; | ||
| this.TaskNameLocation = taskNameLocation; | ||
|
|
||
| // Entities only have a state type parameter, not input/output | ||
| if (kind == DurableTaskKind.Entity) | ||
|
|
@@ -620,6 +703,7 @@ | |
| public string InputParameter { get; } | ||
| public string OutputType { get; } | ||
| public DurableTaskKind Kind { get; } | ||
| public Location? TaskNameLocation { get; } | ||
|
|
||
| public bool IsActivity => this.Kind == DurableTaskKind.Activity; | ||
|
|
||
|
|
@@ -647,14 +731,16 @@ | |
|
|
||
| class DurableEventTypeInfo | ||
| { | ||
| public DurableEventTypeInfo(string eventName, ITypeSymbol eventType) | ||
| public DurableEventTypeInfo(string eventName, ITypeSymbol eventType, Location? eventNameLocation = null) | ||
| { | ||
| this.TypeName = GetRenderedTypeExpression(eventType); | ||
| this.EventName = eventName; | ||
| this.EventNameLocation = eventNameLocation; | ||
| } | ||
|
|
||
| public string TypeName { get; } | ||
| public string EventName { get; } | ||
| public Location? EventNameLocation { get; } | ||
|
|
||
| static string GetRenderedTypeExpression(ITypeSymbol? symbol) | ||
| { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.