Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Generators/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

9 changes: 9 additions & 0 deletions src/Generators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
DURABLE3001 | DurableTask.Design | Error | **DurableTaskSourceGenerator**: Reports when a task name in [DurableTask] attribute is not a valid C# identifier. Task names must start with a letter or underscore and contain only letters, digits, and underscores.
DURABLE3002 | DurableTask.Design | Error | **DurableTaskSourceGenerator**: Reports when an event name in [DurableEvent] attribute is not a valid C# identifier. Event names must start with a letter or underscore and contain only letters, digits, and underscores.
97 changes: 90 additions & 7 deletions src/Generators/DurableTaskSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using System.Linq;
Comment thread
YunchuWang marked this conversation as resolved.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -39,6 +40,32 @@ public class DurableTaskSourceGenerator : IIncrementalGenerator
* }
*/

/// <summary>
/// Diagnostic ID for invalid task names.
/// </summary>
const string InvalidTaskNameDiagnosticId = "DURABLE3001";

/// <summary>
/// Diagnostic ID for invalid event names.
/// </summary>
const string InvalidEventNameDiagnosticId = "DURABLE3002";

static readonly DiagnosticDescriptor InvalidTaskNameRule = new(
InvalidTaskNameDiagnosticId,
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,
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)
{
Expand Down Expand Up @@ -166,13 +193,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
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)
Expand Down Expand Up @@ -204,6 +233,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}

string eventName = eventType.Name;
Location? eventNameLocation = null;

if (attribute.ArgumentList?.Arguments.Count > 0)
{
Expand All @@ -212,10 +242,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
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)
Expand All @@ -230,6 +261,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
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,
Expand All @@ -242,17 +289,43 @@ static void Execute(
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);
}
Comment thread
YunchuWang marked this conversation as resolved.
}
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
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);
}
Comment thread
YunchuWang marked this conversation as resolved.
Outdated
}
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
YunchuWang marked this conversation as resolved.
Dismissed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
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)
IEnumerable<DurableTaskTypeInfo> validTasks = allTasks
.Where(task => IsValidCSharpIdentifier(task.TaskName));

foreach (DurableTaskTypeInfo task in validTasks)
{
if (task.IsActivity)
{
Expand All @@ -268,7 +341,12 @@ static void Execute(
}
}

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;
Expand Down Expand Up @@ -347,7 +425,7 @@ public static class GeneratedDurableTaskExtensions
}

// 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);
Expand Down Expand Up @@ -588,11 +666,13 @@ public DurableTaskTypeInfo(
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)
Expand Down Expand Up @@ -620,6 +700,7 @@ public DurableTaskTypeInfo(
public string InputParameter { get; }
public string OutputType { get; }
public DurableTaskKind Kind { get; }
public Location? TaskNameLocation { get; }

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

Expand Down Expand Up @@ -647,14 +728,16 @@ static string GetRenderedTypeExpression(ITypeSymbol? symbol)

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)
{
Expand Down
Loading
Loading