diff --git a/ANcpLua.Roslyn.Utilities.slnx b/ANcpLua.Roslyn.Utilities.slnx
index fe0e2ef..17549ef 100644
--- a/ANcpLua.Roslyn.Utilities.slnx
+++ b/ANcpLua.Roslyn.Utilities.slnx
@@ -21,6 +21,7 @@
+
@@ -28,4 +29,4 @@
-
\ No newline at end of file
+
diff --git a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer.csproj b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer.csproj
index 326f476..bd52316 100644
--- a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer.csproj
+++ b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer.csproj
@@ -16,5 +16,6 @@
+
diff --git a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/MissingCancellationTokenAnalyzer.cs b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/MissingCancellationTokenAnalyzer.cs
index b8f58b7..dd2fc67 100644
--- a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/MissingCancellationTokenAnalyzer.cs
+++ b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer/MissingCancellationTokenAnalyzer.cs
@@ -2,6 +2,7 @@
using System.Linq;
using ANcpLua.Roslyn.Utilities;
using ANcpLua.Roslyn.Utilities.Analyzers;
+using ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
@@ -12,7 +13,7 @@ namespace ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MissingCancellationTokenAnalyzer : DiagnosticAnalyzerBase
{
- public const string DiagnosticId = "ANCP0001";
+ public const string DiagnosticId = MissingCancellationTokenContract.DiagnosticId;
private static readonly DiagnosticDescriptor s_rule = new(
id: DiagnosticId,
@@ -61,9 +62,9 @@ private static void AnalyzeInvocation(OperationAnalysisContext context)
return;
var properties = ImmutableDictionary.Empty
- .Add(MissingCancellationTokenDiagnosticProperties.ParameterName, parameter.Name)
+ .Add(MissingCancellationTokenContract.ParameterNameProperty, parameter.Name)
.Add(
- MissingCancellationTokenDiagnosticProperties.ParameterIndex,
+ MissingCancellationTokenContract.ParameterIndexProperty,
parameter.Ordinal.ToString(System.Globalization.CultureInfo.InvariantCulture));
context.ReportDiagnostic(
@@ -80,24 +81,6 @@ private static bool IsXunitTestMethod(IMethodSymbol method)
|| method.HasAttribute("Xunit.TheoryAttribute");
}
- private static bool IsDefaultLike(IOperation operation, ITypeSymbol cancellationTokenType)
- {
- operation = operation.UnwrapAllConversions().UnwrapParenthesized();
-
- if (operation.Syntax.IsKind(SyntaxKind.DefaultExpression)
- || operation.Syntax.IsKind(SyntaxKind.DefaultLiteralExpression))
- return true;
-
- return operation is IPropertyReferenceOperation
- {
- Property: { Name: "None", ContainingType: { } containingType }
- }
- && containingType.IsEqualTo(cancellationTokenType);
- }
-}
-
-internal static class MissingCancellationTokenDiagnosticProperties
-{
- public const string ParameterName = nameof(ParameterName);
- public const string ParameterIndex = nameof(ParameterIndex);
+ private static bool IsDefaultLike(IOperation operation, ITypeSymbol cancellationTokenType) =>
+ MissingCancellationTokenContract.IsDefaultCancellationTokenArgument(operation, cancellationTokenType);
}
diff --git a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes.csproj b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes.csproj
index 503b458..54b8d98 100644
--- a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes.csproj
+++ b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes.csproj
@@ -16,5 +16,6 @@
+
diff --git a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/MissingCancellationTokenFixer.cs b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/MissingCancellationTokenFixer.cs
index c27fa40..bc1b289 100644
--- a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/MissingCancellationTokenFixer.cs
+++ b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationCodeFixes/MissingCancellationTokenFixer.cs
@@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using ANcpLua.Roslyn.Utilities.Examples.XunitCancellationAnalyzer;
+using ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
@@ -48,15 +49,15 @@ private static async Task ApplyAsync(
Diagnostic diagnostic,
CancellationToken cancellationToken)
{
- if (!diagnostic.Properties.TryGetValue(DiagnosticPropertyNames.ParameterName, out var parameterName)
+ if (!diagnostic.Properties.TryGetValue(MissingCancellationTokenContract.ParameterNameProperty, out var parameterName)
|| string.IsNullOrWhiteSpace(parameterName)
- || !diagnostic.Properties.TryGetValue(DiagnosticPropertyNames.ParameterIndex, out var parameterIndexText)
+ || !diagnostic.Properties.TryGetValue(MissingCancellationTokenContract.ParameterIndexProperty, out var parameterIndexText)
|| !int.TryParse(parameterIndexText, out var parameterIndex))
return document;
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
var arguments = invocation.ArgumentList.Arguments.ToList();
- var tokenExpression = ParseExpression("TestContext.Current.CancellationToken");
+ var tokenExpression = MissingCancellationTokenContract.CreateReplacementTokenExpression();
var safeParameterName = parameterName ?? string.Empty;
var argumentIndex = arguments.FindIndex(argument =>
@@ -87,9 +88,4 @@ private static async Task ApplyAsync(
return editor.GetChangedDocument();
}
- private static class DiagnosticPropertyNames
- {
- public const string ParameterName = nameof(ParameterName);
- public const string ParameterIndex = nameof(ParameterIndex);
- }
}
diff --git a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared.csproj b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared.csproj
new file mode 100644
index 0000000..45afba0
--- /dev/null
+++ b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared.csproj
@@ -0,0 +1,19 @@
+
+
+ netstandard2.0
+ latest
+ enable
+ disable
+ ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared
+ false
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared/MissingCancellationTokenContract.cs b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared/MissingCancellationTokenContract.cs
new file mode 100644
index 0000000..26531b9
--- /dev/null
+++ b/src/ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared/MissingCancellationTokenContract.cs
@@ -0,0 +1,37 @@
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Operations;
+using ANcpLua.Roslyn.Utilities;
+
+namespace ANcpLua.Roslyn.Utilities.Examples.XunitCancellationShared;
+
+public static class MissingCancellationTokenContract
+{
+ public const string DiagnosticId = "ANCP0001";
+ public const string ParameterNameProperty = "ParameterName";
+ public const string ParameterIndexProperty = "ParameterIndex";
+ public const string ReplacementExpression = "TestContext.Current.CancellationToken";
+
+ public static bool IsDefaultCancellationTokenArgument(IOperation operation, ITypeSymbol cancellationTokenType)
+ {
+ if (operation is null)
+ return false;
+
+ operation = operation.UnwrapAllConversions().UnwrapParenthesized();
+
+ if (operation.Syntax.IsKind(SyntaxKind.DefaultExpression)
+ || operation.Syntax.IsKind(SyntaxKind.DefaultLiteralExpression))
+ return true;
+
+ return operation is IPropertyReferenceOperation
+ {
+ Property: { Name: nameof(CancellationToken.None), ContainingType: { } containingType }
+ } && containingType.IsEqualTo(cancellationTokenType);
+ }
+
+ public static ExpressionSyntax CreateReplacementTokenExpression() =>
+ SyntaxFactory.ParseExpression(ReplacementExpression);
+
+}
diff --git a/src/ANcpLua.Roslyn.Utilities/IncrementalValuesProviderExtensions.cs b/src/ANcpLua.Roslyn.Utilities/IncrementalValuesProviderExtensions.cs
index 5926e94..5a47b54 100644
--- a/src/ANcpLua.Roslyn.Utilities/IncrementalValuesProviderExtensions.cs
+++ b/src/ANcpLua.Roslyn.Utilities/IncrementalValuesProviderExtensions.cs
@@ -278,30 +278,37 @@ public static IncrementalValueProvider SelectAndReportExceptions
+ .Select((value, cancellationToken) =>
{
cancellationToken.ThrowIfCancellationRequested();
try
{
- return (Value: selector(value, cancellationToken), Exception: null);
+ return (selector(value, cancellationToken), null);
+ }
+ catch (OperationCanceledException)
+ {
+ // Cancellation must propagate so Roslyn can abort the pipeline.
+ throw;
}
catch (Exception exception)
{
- return (Value: default(TResult), Exception: (Exception?)exception);
+ return (default, GeneratorErrorInfo.From(exception));
}
});
initializationContext.RegisterSourceOutput(outputWithErrors,
(context, tuple) =>
{
- if (tuple.Exception is not null)
- context.ReportException(id, tuple.Exception);
+ if (tuple.Error is { } error)
+ context.ReportException(id, error);
});
- return outputWithErrors
- .Select(static (x, _) =>
- x.Value ?? throw new InvalidOperationException("Unexpected null value in SelectAndReportExceptions"));
+ // The diagnostic registered above is the visible failure for the user. Downstream
+ // consumers receive the selector's value (or default(TResult) on failure) and decide
+ // how to react — record-struct sentinels like FileWithName.Empty, for instance, are
+ // already filtered by AddSource. Throwing here would crash the generator a second time.
+ return outputWithErrors.Select(static (x, _) => x.Value!);
}
///
@@ -450,35 +457,36 @@ public static IncrementalValuesProvider SelectAndReportExceptions
+ .Select((value, cancellationToken) =>
{
cancellationToken.ThrowIfCancellationRequested();
try
{
- return (Value: selector(value, cancellationToken), Exception: null);
+ return (selector(value, cancellationToken), null);
+ }
+ catch (OperationCanceledException)
+ {
+ // Cancellation must propagate so Roslyn can abort the pipeline.
+ throw;
}
catch (Exception exception)
{
- return (Value: default(TResult), Exception: (Exception?)exception);
+ return (default, GeneratorErrorInfo.From(exception));
}
});
initializationContext.RegisterSourceOutput(outputWithErrors
- .Where(static x => x.Exception is not null),
+ .Where(static x => x.Error is not null),
(context, tuple) =>
{
- context.ReportException(id,
- tuple.Exception ??
- throw new InvalidOperationException(
- "Unexpected null exception in SelectAndReportExceptions"));
+ if (tuple.Error is { } error)
+ context.ReportException(id, error);
});
return outputWithErrors
- .Where(static x => x.Exception is null)
- .Select(static (x, _) => x.Value ??
- throw new InvalidOperationException(
- "Unexpected null value in SelectAndReportExceptions"));
+ .Where(static x => x.Error is null)
+ .Select(static (x, _) => x.Value!);
}
///
@@ -519,7 +527,12 @@ public static IncrementalValuesProvider SelectAndReportExceptions.Default;
return source.Collect().SelectMany((values, _) =>
{
+ // Preserve key-insertion order so generator output is deterministic.
+ // Dictionary<,> enumeration order is not part of the contract (it happens
+ // to follow insertion in the current .NET runtime, but a future runtime
+ // is free to change that, and adding/removing entries can shift it today).
var map = new Dictionary.Builder>(comparer);
+ var keys = new List();
foreach (var value in values)
{
var key = keySelector(value);
@@ -527,14 +540,15 @@ public static IncrementalValuesProvider SelectAndReportExceptions();
map.Add(key, builder);
+ keys.Add(key);
}
builder.Add(elementSelector(value));
}
- var result = ImmutableArray.CreateBuilder<(TKey, EquatableArray)>(map.Count);
- foreach (var entry in map)
- result.Add((entry.Key, entry.Value.ToImmutable().AsEquatableArray()));
+ var result = ImmutableArray.CreateBuilder<(TKey, EquatableArray)>(keys.Count);
+ foreach (var key in keys)
+ result.Add((key, map[key].ToImmutable().AsEquatableArray()));
return result.MoveToImmutable();
});
}
diff --git a/src/ANcpLua.Roslyn.Utilities/Models/GeneratorErrorInfo.cs b/src/ANcpLua.Roslyn.Utilities/Models/GeneratorErrorInfo.cs
new file mode 100644
index 0000000..9506493
--- /dev/null
+++ b/src/ANcpLua.Roslyn.Utilities/Models/GeneratorErrorInfo.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ANcpLua. All rights reserved.
+// Licensed under the MIT License.
+
+namespace ANcpLua.Roslyn.Utilities.Models;
+
+///
+/// A value-equatable snapshot of an safe to flow through the
+/// incremental generator cache.
+///
+///
+///
+/// instances do not implement value equality, so storing one in
+/// pipeline state forces every cache comparison to reference-equality and breaks
+/// incremental caching across compilations. captures the
+/// minimum information needed to surface the failure as a diagnostic — type name, message,
+/// and stack trace — as immutable strings, which the record-struct equality contract
+/// compares by value.
+///
+///
+/// The exception's runtime type name (e.g. System.InvalidOperationException).
+/// The exception's message, or empty if absent.
+/// The exception's stack trace, or empty if absent.
+#if ANCPLUA_ROSLYN_PUBLIC
+public
+#else
+internal
+#endif
+ readonly record struct GeneratorErrorInfo(string TypeName, string Message, string StackTrace)
+{
+ ///
+ /// Captures an as a value-equatable .
+ ///
+ public static GeneratorErrorInfo From(Exception exception)
+ {
+ if (exception is null) throw new ArgumentNullException(nameof(exception));
+ return new GeneratorErrorInfo(
+ exception.GetType().FullName ?? exception.GetType().Name,
+ exception.Message ?? string.Empty,
+ exception.StackTrace ?? string.Empty);
+ }
+
+ ///
+ /// Renders the captured error in Type: Message\n StackTrace form, mirroring
+ /// .
+ ///
+ public override string ToString()
+ {
+ return string.IsNullOrEmpty(StackTrace)
+ ? $"{TypeName}: {Message}"
+ : $"{TypeName}: {Message}{Environment.NewLine}{StackTrace}";
+ }
+}
diff --git a/src/ANcpLua.Roslyn.Utilities/SourceProductionContextExtensions.cs b/src/ANcpLua.Roslyn.Utilities/SourceProductionContextExtensions.cs
index 4f09f54..50c6446 100644
--- a/src/ANcpLua.Roslyn.Utilities/SourceProductionContextExtensions.cs
+++ b/src/ANcpLua.Roslyn.Utilities/SourceProductionContextExtensions.cs
@@ -183,6 +183,38 @@ public static void ReportException(
context.ReportDiagnostic(exception.ToDiagnostic(id, prefix));
}
+ ///
+ /// Reports a captured as an error diagnostic.
+ ///
+ ///
+ /// Use this overload from incremental pipeline stages so the captured error can flow
+ /// through cache state by value rather than as a reference-equatable
+ /// .
+ ///
+ /// The source production context to report the diagnostic to.
+ /// The diagnostic ID (e.g., "GEN001").
+ /// The captured error to report.
+ /// Optional prefix prepended to .
+ public static void ReportException(
+ this SourceProductionContext context,
+ string id,
+ GeneratorErrorInfo error,
+ string? prefix = null)
+ {
+ id = id ?? throw new ArgumentNullException(nameof(id));
+ if (prefix is not null) id = $"{prefix}{id}";
+
+ context.ReportDiagnostic(Diagnostic.Create(
+ new DiagnosticDescriptor(
+ id,
+ "Exception: ",
+ error.ToString(),
+ "Usage",
+ DiagnosticSeverity.Error,
+ true),
+ Location.None));
+ }
+
///
/// Creates a from an exception.
///
diff --git a/tests/ANcpLua.Roslyn.Utilities.Testing.Tests/IncrementalValuesProviderExtensionsTests.cs b/tests/ANcpLua.Roslyn.Utilities.Testing.Tests/IncrementalValuesProviderExtensionsTests.cs
new file mode 100644
index 0000000..4dc080e
--- /dev/null
+++ b/tests/ANcpLua.Roslyn.Utilities.Testing.Tests/IncrementalValuesProviderExtensionsTests.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using ANcpLua.Roslyn.Utilities;
+using ANcpLua.Roslyn.Utilities.Models;
+using AwesomeAssertions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Xunit;
+
+namespace ANcpLua.Roslyn.Utilities.Testing.Tests;
+
+///
+/// Regression tests for behaviors that affect
+/// generator correctness: cache stability, cancellation propagation, deterministic emission order.
+///
+public sealed class IncrementalValuesProviderExtensionsTests
+{
+ [Fact]
+ public void GeneratorErrorInfo_From_CapturesType_Message_AndStackTrace()
+ {
+ var caught = CaptureThrow();
+
+ var info = GeneratorErrorInfo.From(caught);
+
+ info.TypeName.Should().Be(typeof(InvalidOperationException).FullName);
+ info.Message.Should().Be("boom");
+ info.StackTrace.Should().NotBeNullOrEmpty();
+ info.StackTrace.Should().Contain(nameof(ThrowDeep));
+ info.ToString().Should().StartWith($"{typeof(InvalidOperationException).FullName}: boom");
+ }
+
+ [Fact]
+ public void GeneratorErrorInfo_HasValueEquality_SoItIsCacheStable()
+ {
+ // Two distinct Exception instances with identical surface produce equal GeneratorErrorInfos.
+ // This is the property that makes the type safe to flow through the incremental cache.
+ var first = CaptureThrow();
+ var second = CaptureThrow();
+
+ var a = GeneratorErrorInfo.From(first) with { StackTrace = "fixed" };
+ var b = GeneratorErrorInfo.From(second) with { StackTrace = "fixed" };
+
+ a.Should().Be(b);
+ a.GetHashCode().Should().Be(b.GetHashCode());
+ ReferenceEquals(first, second).Should().BeFalse();
+ }
+
+ [Fact]
+ public void GroupBy_PreservesKeyInsertionOrder()
+ {
+ // The previous implementation iterated a Dictionary directly, which has no documented
+ // iteration order. Generators that consume GroupBy output must see a stable, deterministic
+ // sequence so generated source is reproducible across runs.
+ var observed = RunGroupByGenerator(GroupByOrderingGenerator.Source);
+
+ observed.Should().Equal("Beta", "Alpha", "Gamma");
+ }
+
+ [Fact]
+ public void SelectAndReportExceptions_ValuesOverload_PropagatesCancellation()
+ {
+ // The pre-cancelled token is the *system under test* — using
+ // TestContext.Current.CancellationToken would defeat the purpose, so xUnit1051 is
+ // suppressed for this test only.
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ var driver = CSharpGeneratorDriver.Create(new CancellingGenerator());
+ var ct = TestContext.Current.CancellationToken;
+ var compilation = CSharpCompilation.Create(
+ "Test",
+ [CSharpSyntaxTree.ParseText("class C { }", cancellationToken: ct)],
+ references: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]);
+
+#pragma warning disable xUnit1051
+ Action act = () => driver.RunGenerators(compilation, cts.Token);
+#pragma warning restore xUnit1051
+
+ act.Should().Throw();
+ }
+
+ private static InvalidOperationException CaptureThrow()
+ {
+ try
+ {
+ ThrowDeep();
+ throw new InvalidOperationException("unreachable");
+ }
+ catch (InvalidOperationException ex)
+ {
+ return ex;
+ }
+ }
+
+ private static void ThrowDeep()
+ {
+ throw new InvalidOperationException("boom");
+ }
+
+ private static List RunGroupByGenerator(string source)
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var compilation = CSharpCompilation.Create(
+ "Test",
+ [CSharpSyntaxTree.ParseText(source, cancellationToken: ct)],
+ references: [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new GroupByOrderingGenerator());
+ driver = driver.RunGenerators(compilation, ct);
+ var result = driver.GetRunResult().Results.Single();
+ result.Exception.Should().BeNull();
+
+ return GroupByOrderingGenerator.LastObservedKeys;
+ }
+
+ ///
+ /// Probes the emission
+ /// order by feeding a fixed sequence and recording the keys back in a static field.
+ ///
+ private sealed class GroupByOrderingGenerator : IIncrementalGenerator
+ {
+ public const string Source = """
+ namespace Probe
+ {
+ class Marker { }
+ }
+ """;
+
+ public static List LastObservedKeys { get; } = [];
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ LastObservedKeys.Clear();
+
+ var seed = context.CompilationProvider.SelectMany(static (_, _) =>
+ ImmutableArray.Create(
+ new Item("Beta", 1),
+ new Item("Alpha", 1),
+ new Item("Beta", 2),
+ new Item("Gamma", 1),
+ new Item("Alpha", 2)));
+
+ var grouped = seed.GroupBy(static x => x.Key, static x => x.Value);
+
+ context.RegisterSourceOutput(grouped, static (_, group) =>
+ {
+ lock (LastObservedKeys)
+ {
+ LastObservedKeys.Add(group.Key);
+ }
+ });
+ }
+
+ private readonly record struct Item(string Key, int Value);
+ }
+
+ ///
+ /// Drives with a selector that observes the cancellation token to confirm cancellation flows out
+ /// of the pipeline rather than being swallowed.
+ ///
+ private sealed class CancellingGenerator : IIncrementalGenerator
+ {
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var seed = context.CompilationProvider
+ .SelectMany(static (_, _) => ImmutableArray.Create(0));
+
+ seed.SelectAndReportExceptions(
+ static (_, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+ return FileWithName.Empty;
+ },
+ context)
+ .AddSource(context);
+ }
+ }
+}