Diagnostics and Compile Time Validation - Garume/Manifold GitHub Wiki
The Manifold source generator performs compile-time validation of all [Operation]-attributed code. When it detects invalid or ambiguous operation definitions, it emits diagnostics with the DMCF prefix that surface as compiler errors. These diagnostics prevent malformed operations from producing incorrect generated code, catching configuration mistakes at build time rather than at runtime.
All five diagnostic rules (DMCF001–DMCF005) are defined and enforced within OperationDescriptorGenerator in the Manifold.Generators package. They are organized into three categories: visibility conflicts, parameter binding errors, and class-based operation structural requirements. This page serves as the authoritative reference for each rule, its trigger conditions, and how to resolve violations. For attribute usage details, see Attributes and Operation Definition. For the broader generator architecture, see Source Generator — Manifold.Generators.
| Rule ID | Title | Applies To | Severity | Category |
|---|---|---|---|---|
| DMCF001 | Conflicting CLI and MCP visibility | Static methods, classes | Error | Manifold |
| DMCF002 | Conflicting parameter binding | Method parameters, request properties | Error | Manifold |
| DMCF003 | Unsupported parameter binding | Method parameters only | Error | Manifold |
| DMCF004 | Unsupported operation class | Class-based operations | Error | Manifold |
| DMCF005 | Unsupported request property binding | Request type properties | Error | Manifold |
Sources: Manifold.Generators/OperationDescriptorGenerator.cs:27–61
The diagnostic validation is integrated into the incremental source generator pipeline. The generator analyzes each [Operation]-attributed symbol, collecting diagnostics alongside valid operation candidates. Diagnostics are reported before any code is emitted, ensuring that invalid operations never produce generated artifacts.
flowchart TD
A["[Operation] Attributed Symbol"] --> B{"Symbol Kind?"}
B -->|Method| C["CreateMethodCandidate"]
B -->|Class| D["CreateClassCandidate"]
C --> E["Validate Visibility\nDMCF001"]
D --> F["Validate Visibility\nDMCF001"]
E --> G["Validate Parameters\nDMCF002, DMCF003"]
F --> H["Validate Interface\nDMCF004"]
H --> I["Validate Properties\nDMCF002, DMCF005"]
G --> J{"Any Diagnostics?"}
I --> J
J -->|Yes| K["Report via\nReportDiagnostic"]
J -->|No| L["Emit Generated Code"]
K --> M["Build Error"]
Sources: Manifold.Generators/OperationDescriptorGenerator.cs:78–91
The CreateCandidate method dispatches based on symbol kind:
private static OperationAnalysisResult CreateCandidate(
GeneratorAttributeSyntaxContext context,
CancellationToken cancellationToken)
{
AttributeData operationAttribute = context.Attributes[0];
return context.TargetSymbol switch
{
IMethodSymbol methodSymbol => CreateMethodCandidate(context, methodSymbol, operationAttribute, cancellationToken),
INamedTypeSymbol typeSymbol => CreateClassCandidate(context, typeSymbol, operationAttribute, cancellationToken),
_ => new OperationAnalysisResult(null, [])
};
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:78–91
All collected diagnostics are reported in the Execute method before any source generation occurs. If any diagnostics exist for an operation, no candidate is produced for that operation — effectively excluding it from code generation.
foreach (OperationAnalysisResult analysisResult in collectedCandidates)
{
foreach (OperationDiagnostic diagnostic in analysisResult.Diagnostics)
context.ReportDiagnostic(Diagnostic.Create(
diagnostic.Descriptor, diagnostic.Location, diagnostic.MessageArgs));
if (analysisResult.Candidate is not null)
operations.Add(analysisResult.Candidate);
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:620–633
Message: Operation '{0}' cannot be marked with both [CliOnly] and [McpOnly]
An operation cannot be simultaneously restricted to CLI-only and MCP-only surfaces. The [CliOnly] and [McpOnly] attributes are mutually exclusive — an operation must be visible on both surfaces (the default), restricted to one, or hidden entirely via the Hidden property on [Operation].
This diagnostic is emitted when both [CliOnly] and [McpOnly] attributes are present on:
- A static method decorated with
[Operation] - A class decorated with
[Operation]
Both CreateMethodCandidate and CreateClassCandidate perform the same check:
bool hasCliOnly = HasAttribute(methodSymbol, CliOnlyAttributeMetadataName);
bool hasMcpOnly = HasAttribute(methodSymbol, McpOnlyAttributeMetadataName);
if (hasCliOnly && hasMcpOnly)
{
diagnosticBuilder.Add(new OperationDiagnostic(
ConflictingVisibilityDescriptor,
GetBestLocation(methodSymbol),
[methodSymbol.Name]));
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:101–109, Manifold.Generators/OperationDescriptorGenerator.cs:167–175
[Operation("sample.conflict")]
[CliOnly]
[McpOnly]
public static string Conflict([Argument(0)] string name) => name;
// Error DMCF001: Operation 'Conflict' cannot be marked with both [CliOnly] and [McpOnly]Remove one of the two attributes. Use [CliOnly] to restrict the operation to the CLI surface, or [McpOnly] to restrict it to the MCP surface. To hide an operation from both surfaces, use [Operation("id", Hidden = true)] instead.
Sources: Manifold.Generators.Tests/OperationDescriptorGeneratorDiagnosticsTests.cs:11–30
Message: Parameter '{0}' on operation '{1}' cannot be marked with both [Option] and [Argument]
A parameter or request property cannot be bound as both an option (named --flag style) and a positional argument simultaneously. Each parameter must use exactly one binding strategy.
This diagnostic is emitted when both [Option] and [Argument] attributes are present on:
- A method parameter in a static-method operation
- A property on a request type in a class-based operation
For method parameters (CreateParameterCandidate):
AttributeData? optionAttribute = GetAttribute(parameterSymbol, OptionAttributeMetadataName);
AttributeData? argumentAttribute = GetAttribute(parameterSymbol, ArgumentAttributeMetadataName);
if (optionAttribute is not null && argumentAttribute is not null)
{
return ParameterAnalysisResult.FromDiagnostic(
new OperationDiagnostic(
ConflictingParameterBindingDescriptor,
GetBestLocation(parameterSymbol),
[parameterSymbol.Name, operationName]));
}The same check is performed for request type properties in CreatePropertyCandidate.
Sources: Manifold.Generators/OperationDescriptorGenerator.cs:295–305, Manifold.Generators/OperationDescriptorGenerator.cs:360–370
[Operation("sample.conflict-parameter")]
public static string Conflict([Option("name")][Argument(0)] string name) => name;
// Error DMCF002: Parameter 'name' on operation 'Conflict' cannot be marked
// with both [Option] and [Argument]Choose either [Option("name")] for named binding or [Argument(0)] for positional binding. See Parameter Binding and Type Conversion for guidance on which to use.
Sources: Manifold.Generators.Tests/OperationDescriptorGeneratorDiagnosticsTests.cs:33–50
Message: Parameter '{0}' on operation '{1}' must be bound with [Option], [Argument], [FromServices], or be a CancellationToken
Every parameter on a static-method operation must declare how it receives its value. The generator requires each parameter to be one of:
- A named option via
[Option] - A positional argument via
[Argument] - A service injection via
[FromServices] - A
CancellationToken(recognized automatically)
A bare, unattributed parameter is ambiguous and cannot be bound by either CLI parsing or MCP JSON deserialization.
This diagnostic is emitted when a method parameter has none of [Option], [Argument], [FromServices], and is not a CancellationToken.
if (!isCancellationToken && !hasFromServices && optionAttribute is null && argumentAttribute is null)
{
return ParameterAnalysisResult.FromDiagnostic(
new OperationDiagnostic(
UnsupportedParameterBindingDescriptor,
GetBestLocation(parameterSymbol),
[parameterSymbol.Name, operationName]));
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:307–314
[Operation("sample.unbound")]
public static string Unbound(string name) => name;
// Error DMCF003: Parameter 'name' on operation 'Unbound' must be bound
// with [Option], [Argument], [FromServices], or be a CancellationTokenAdd one of the required binding attributes:
// As a named option:
public static string Unbound([Option("name")] string name) => name;
// As a positional argument:
public static string Unbound([Argument(0)] string name) => name;
// As an injected service:
public static string Unbound([FromServices] IMyService service) => service.GetName();Note that this diagnostic applies only to static-method operations. Class-based operations bind parameters through request type properties, which are validated separately. See Dependency Injection and Service Resolution for [FromServices] usage.
Sources: Manifold.Generators.Tests/OperationDescriptorGeneratorDiagnosticsTests.cs:53–70
Message: Operation class '{0}' must implement IOperation<TRequest, TResult> and expose ExecuteAsync(TRequest, OperationContext)
A class decorated with [Operation] must follow the class-based operation contract. This requires two conditions:
- The class must implement
IOperation<TRequest, TResult> - The class must expose a non-static
ExecuteAsync(TRequest, OperationContext)method
This diagnostic catches classes that are decorated as operations but do not conform to the expected shape.
DMCF004 is emitted in three scenarios within CreateClassCandidate:
-
Missing interface implementation — the class does not implement
IOperation<TRequest, TResult> - Missing ExecuteAsync method — the class implements the interface but lacks the expected method signature
-
Non-named request type — the request type argument is not an
INamedTypeSymbol(fallback case)
flowchart TD
A["Class with [Operation]"] --> B{"Implements\nIOperation?"}
B -->|No| C["DMCF004:\nMissing interface"]
B -->|Yes| D{"Has ExecuteAsync\nmethod?"}
D -->|No| E["DMCF004:\nMissing method"]
D -->|Yes| F{"Request type is\nINamedTypeSymbol?"}
F -->|No| G["DMCF004:\nInvalid request type"]
F -->|Yes| H["Validate Properties"]
The generator performs two sequential checks:
// Check 1: Interface implementation
INamedTypeSymbol? operationInterface = typeSymbol.AllInterfaces.FirstOrDefault(
static candidate => string.Equals(
candidate.OriginalDefinition.ToDisplayString(),
OperationInterfaceMetadataName, StringComparison.Ordinal));
if (operationInterface is null)
{
diagnosticBuilder.Add(new OperationDiagnostic(
UnsupportedOperationClassDescriptor,
GetBestLocation(typeSymbol),
[typeSymbol.Name]));
return new OperationAnalysisResult(null, diagnosticBuilder.ToImmutable());
}
// Check 2: ExecuteAsync method
IMethodSymbol? executeMethod = FindExecuteMethod(typeSymbol, requestType);
if (executeMethod is null)
{
diagnosticBuilder.Add(new OperationDiagnostic(
UnsupportedOperationClassDescriptor,
GetBestLocation(typeSymbol),
[typeSymbol.Name]));
return new OperationAnalysisResult(null, diagnosticBuilder.ToImmutable());
}The FindExecuteMethod helper looks for a specific method shape:
private static IMethodSymbol? FindExecuteMethod(
INamedTypeSymbol typeSymbol, ITypeSymbol requestType)
{
return typeSymbol.GetMembers("ExecuteAsync")
.OfType<IMethodSymbol>()
.FirstOrDefault(methodSymbol =>
!methodSymbol.IsStatic &&
methodSymbol.Parameters.Length == 2 &&
SymbolEqualityComparer.Default.Equals(
methodSymbol.Parameters[0].Type, requestType) &&
string.Equals(
methodSymbol.Parameters[1].Type.ToDisplayString(),
OperationContextMetadataName, StringComparison.Ordinal));
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:177–197, Manifold.Generators/OperationDescriptorGenerator.cs:250–254, Manifold.Generators/OperationDescriptorGenerator.cs:257–266
[Operation("sample.invalid-class")]
public sealed class InvalidOperation
{
// Missing IOperation<TRequest, TResult> implementation
// Missing ExecuteAsync method
}
// Error DMCF004: Operation class 'InvalidOperation' must implement
// IOperation<TRequest, TResult> and expose ExecuteAsync(TRequest, OperationContext)Implement the IOperation<TRequest, TResult> interface and provide the ExecuteAsync method:
[Operation("sample.valid-class")]
public sealed class ValidOperation : IOperation<ValidOperation.Request, string>
{
public ValueTask<string> ExecuteAsync(Request request, OperationContext context)
{
return ValueTask.FromResult(request.Name);
}
public sealed class Request
{
[Option("name")]
public string Name { get; init; } = "";
}
}See Core Contracts — Manifold Package for the IOperation interface definition.
Sources: Manifold.Generators.Tests/OperationDescriptorGeneratorDiagnosticsTests.cs:73–86
Message: Property '{0}' on request type '{1}' for operation '{2}' must be writable with a public init or set accessor
In class-based operations, the generator populates request type properties by setting their values during parameter binding. Properties decorated with [Option] or [Argument] must therefore be writable — they must have a public set or init accessor. Read-only properties (expression-bodied, get-only) cannot be populated by the generated binding code.
This diagnostic is emitted when a request type property decorated with [Option] or [Argument] either:
- Has no setter at all (e.g., expression-bodied
=> "value"or{ get; }) - Has a non-public setter (e.g.,
{ get; private set; })
if (propertySymbol.SetMethod is null ||
propertySymbol.SetMethod.DeclaredAccessibility != Accessibility.Public)
{
return ParameterAnalysisResult.FromDiagnostic(
new OperationDiagnostic(
UnsupportedRequestPropertyBindingDescriptor,
GetBestLocation(propertySymbol),
[propertySymbol.Name, requestTypeSymbol.Name, operationName]));
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:372–379
[Operation("sample.invalid-request")]
public sealed class InvalidRequestOperation : IOperation<InvalidRequestOperation.Request, string>
{
public ValueTask<string> ExecuteAsync(Request request, OperationContext context)
=> ValueTask.FromResult(request.Name);
public sealed class Request
{
[Option("name")]
public string Name => "blocked"; // Expression-bodied, no setter
}
}
// Error DMCF005: Property 'Name' on request type 'Request' for operation
// 'InvalidRequestOperation' must be writable with a public init or set accessorAdd a public init or set accessor to the property:
public sealed class Request
{
[Option("name")]
public string Name { get; init; } = ""; // init accessor — preferred
// Or: public string Name { get; set; } = "";
}Sources: Manifold.Generators.Tests/OperationDescriptorGeneratorDiagnosticsTests.cs:89–112
Not all diagnostics apply to both operation authoring styles. The following table shows which diagnostics can fire for static-method operations versus class-based operations:
| Rule | Static-Method Operations | Class-Based Operations |
|---|---|---|
| DMCF001 | Yes — on the method | Yes — on the class |
| DMCF002 | Yes — on method parameters | Yes — on request properties |
| DMCF003 | Yes — on method parameters | No — not applicable |
| DMCF004 | No — not applicable | Yes — on the class |
| DMCF005 | No — not applicable | Yes — on request properties |
flowchart TD
subgraph SM["Static-Method Operations"]
SM1["DMCF001\nVisibility"]
SM2["DMCF002\nParam Binding"]
SM3["DMCF003\nUnbound Param"]
end
subgraph CB["Class-Based Operations"]
CB1["DMCF001\nVisibility"]
CB2["DMCF002\nProp Binding"]
CB4["DMCF004\nMissing Interface"]
CB5["DMCF005\nRead-Only Prop"]
end
A["[Operation] Attribute"] --> SM
A --> CB
Sources: Manifold.Generators/OperationDescriptorGenerator.cs:93–157, Manifold.Generators/OperationDescriptorGenerator.cs:159–255
The diagnostic system uses three internal types to collect and transport diagnostic information through the generator pipeline.
Wraps a Roslyn DiagnosticDescriptor with a source location and message arguments for deferred reporting:
private sealed class OperationDiagnostic
{
public DiagnosticDescriptor Descriptor { get; }
public Location? Location { get; }
public object?[] MessageArgs { get; }
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2754–2769
Pairs an optional valid OperationCandidate with any diagnostics discovered during analysis. When diagnostics are present, the candidate is null:
private sealed class OperationAnalysisResult
{
public OperationCandidate? Candidate { get; }
public ImmutableArray<OperationDiagnostic> Diagnostics { get; }
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2678–2690
Represents either a successfully validated parameter or a diagnostic. Created via factory methods to enforce the mutual exclusion:
private sealed class ParameterAnalysisResult
{
public ParameterCandidate? Candidate { get; }
public ImmutableArray<OperationDiagnostic> Diagnostics { get; }
public static ParameterAnalysisResult FromCandidate(ParameterCandidate candidate);
public static ParameterAnalysisResult FromDiagnostic(OperationDiagnostic diagnostic);
}Sources: Manifold.Generators/OperationDescriptorGenerator.cs:2730–2752
classDiagram
class OperationAnalysisResult {
+OperationCandidate? Candidate
+ImmutableArray~OperationDiagnostic~ Diagnostics
}
class ParameterAnalysisResult {
+ParameterCandidate? Candidate
+ImmutableArray~OperationDiagnostic~ Diagnostics
+FromCandidate()$ ParameterAnalysisResult
+FromDiagnostic()$ ParameterAnalysisResult
}
class OperationDiagnostic {
+DiagnosticDescriptor Descriptor
+Location? Location
+object?[] MessageArgs
}
OperationAnalysisResult --> OperationDiagnostic
ParameterAnalysisResult --> OperationDiagnostic
All five diagnostic rules have dedicated unit tests in OperationDescriptorGeneratorDiagnosticsTests. Each test compiles a snippet of C# code through the generator and asserts that the expected diagnostic is emitted with the correct rule ID and message fragment.
| Test Method | Validates |
|---|---|
Run_generates_diagnostic_for_conflicting_visibility |
DMCF001 |
Run_generates_diagnostic_for_conflicting_parameter_binding |
DMCF002 |
Run_generates_diagnostic_for_unbound_parameter |
DMCF003 |
Run_generates_diagnostic_for_invalid_operation_class_shape |
DMCF004 |
Run_generates_diagnostic_for_read_only_request_property |
DMCF005 |
The test infrastructure creates a CSharpCompilation with the test source and runs the generator via CSharpGeneratorDriver, then inspects GetRunResult().Diagnostics for the expected entries.
Sources: Manifold.Generators.Tests/OperationDescriptorGeneratorDiagnosticsTests.cs:1–139
- Source Generator — Manifold.Generators — Full documentation of the incremental source generator pipeline
- Attributes and Operation Definition — Reference for all attributes validated by these diagnostics
-
Core Contracts — Manifold Package —
IOperation<TRequest, TResult>interface required by DMCF004 - Parameter Binding and Type Conversion — How parameters are bound at runtime after passing validation
-
Dependency Injection and Service Resolution —
[FromServices]attribute relevant to DMCF003 - Sample Operations Reference — Working examples of valid operation definitions
- Testing Strategy — Test infrastructure used for diagnostic validation tests
- Architecture Validation and Code Quality — Broader code quality enforcement beyond compile-time diagnostics