Railway-oriented programming for source generator pipelines. Never lose a diagnostic.
The Problem
Traditional generator code loses diagnostics:
// BAD: Diagnostics get lost
var model = ExtractModel(syntax);
if (model == null) return; // Where's the diagnostic?
var validated = Validate(model);
if (!validated.Success) return; // Lost again!
The Solution
DiagnosticFlow<T> carries both value AND diagnostics through the pipeline:
// GOOD: Diagnostics flow through
symbol.ToFlow(nullDiag)
.Then(ExtractModel)
.Then(Validate)
.Then(Generate)
.ReportAndContinue(context);
Creating Flows
// From value
var flow = DiagnosticFlow.Ok(value);
// From nullable (fails if null)
var flow = symbol.ToFlow(nullDiag);
// With initial diagnostics
var flow = DiagnosticFlow.Ok(value, warnings);
// Failed flow
var flow = DiagnosticFlow.Fail<T>(errorDiag);
Chaining Operations
flow.Then(value => TransformValue(value))
.Then(transformed => AnotherTransform(transformed));
Select - Map Without Flow
flow.Select(value => value.Name)
.Select(name => name.ToUpperInvariant());
Where - Filter with Diagnostic
flow.Where(
predicate: m => m.IsAsync,
onFail: asyncRequiredDiag
);
WarnIf - Conditional Warning
flow.WarnIf(
predicate: m => m.IsObsolete,
warning: obsoleteWarning
);
Combining Flows
// Tuple of two flows (both must succeed)
var combined = DiagnosticFlow.Zip(flow1, flow2);
// Result: DiagnosticFlow<(T1, T2)>
// Collect all (all must succeed, diagnostics accumulated)
var all = DiagnosticFlow.Collect(flows);
// Result: DiagnosticFlow<ImmutableArray<T>>
Result Handling
// Get value or default
var value = flow.ValueOrDefault(fallback);
// Pattern match
flow.Match(
onSuccess: value => HandleSuccess(value),
onFailure: diagnostics => HandleFailure(diagnostics)
);
// Execute side effect
flow.Do(value => LogValue(value));
Pipeline Integration
Use with IncrementalValuesProvider:
var pipeline = context.SyntaxProvider
.ForAttributeWithMetadataName("MyAttribute", ...)
.SelectFlow(ctx => ExtractModel(ctx))
.ThenFlow(model => Validate(model))
.WarnIf(model => model.IsDeprecated, deprecatedWarn)
.ReportAndContinue(context)
.Select(model => GenerateCode(model));
context.RegisterSourceOutput(pipeline, (ctx, code) =>
ctx.AddSource(code.Name, code.Content));
Properties
| Property | Description |
|---|
IsSuccess | True if no errors |
IsFailed | True if has errors |
HasErrors | True if any error-severity diagnostic |
Value | The wrapped value (throws if failed) |
Diagnostics | All accumulated diagnostics |
Null-Safety
DiagnosticFlow<T> uses [MemberNotNullWhen] to enable flow-dependent null analysis:
var flow = symbol.ToFlow(nullDiag);
// Compiler knows Value is non-null when IsSuccess is true
if (flow.IsSuccess)
{
var name = flow.Value.Name; // No null warning
}
// Pattern matching also works
if (flow is { IsSuccess: true, Value: var value })
{
var name = value.Name; // No null warning
}
The [MemberNotNullWhen(true, nameof(Value))] attribute on IsSuccess tells
the compiler that Value is guaranteed non-null when IsSuccess returns
true.