-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Background / Why
TinyBDD now ships its source generator transitively (packed into the main TinyBDD NuGet as an analyzer). In src/TinyBDD/TinyBDD.csproj, we:
- reference the generator project as an analyzer (
OutputItemType="Analyzer",ReferenceOutputAssembly="false") - and manually pack the analyzer DLL into
analyzers/dotnet/cs
This means upgrading TinyBDD can suddenly enable source generation for existing projects without any explicit opt-in.
Today the generator produces code that includes a partial class {ContainingType.Name} (see src/TinyBDD.SourceGenerators/OptimizedCodeGenerator.cs). If the user’s containing type is not partial, compilation fails (classic “you forgot the partial keyword” problem), which is exactly the kind of upgrade foot-gun we want to avoid.
Problem Statement
Current behavior (simplified):
TinyBddOptimizerselects methods that look like BDD scenarios (broad predicate).- It generates an additional partial type containing
{MethodName}_Optimized(...). - If the user’s type is not
partial, the generatedpartial classcauses a compile error.
Desired behavior:
- Only emit generated optimized code when the containing type is an “expected partial”.
- If it isn’t, do not generate and instead emit build warnings explaining how to enable optimization (make the type
partial) or how to silence/opt-out.
Goals
- Upgrade-safe: no compilation breaks when users update TinyBDD.
- Actionable warnings: clear, consistent diagnostics that tell users exactly what to do.
- No ambiguity for implementors: explicit rules, explicit test coverage.
Non-Goals
- We are not changing the semantics of the optimization output (just gating generation).
- We are not implementing a CodeFix (optional future enhancement).
- We are not redesigning the optimization strategy (though we’ll capture a couple optional follow-ups).
Proposed Change
1) Define “Expected Partial” (Generation Eligibility)
A method is eligible for generated optimized output only if all of the following are true:
Type-level constraints
- The containing type is a
classorrecord class(and is supported by our generator output). - The containing type is declared
partialin every declaration (for safety in multi-partial scenarios). - The containing type is not nested (
ContainingType.ContainingType == null)
Reason: generator output currently only uses.ContainingType.Nameand does not reproduce nesting; nested types would generate incorrect/duplicate top-level types. - The containing type is not generic (no type parameters)
Reason: generator currently emitspartial class Foorather thanpartial class Foo<T>.
Method-level constraints
- Method passes existing selection logic (async Task/ValueTask shape + looks like BDD chain + not opted out).
If any type-level constraint fails: skip generation and emit diagnostics (below).
2) Add Diagnostics (Warnings) Instead of Generating
Introduce the following diagnostics (IDs, messages, locations, and suppression behavior must be deterministic and stable):
| ID | Severity | Title | Message Format | Applies When | Location |
|---|---|---|---|---|---|
| TBDD010 | Warning | Optimization skipped: containing type is not partial | TinyBDD optimization was skipped for '{0}.{1}' because containing type '{0}' is not declared partial. Mark '{0}' as partial to enable source-generated optimizations (or opt out via [DisableOptimization]). |
Selected method would be optimized, but the containing type does not include partial |
Type identifier location (preferred) or method identifier |
| TBDD011 | Warning | Optimization skipped: nested types not supported | TinyBDD optimization was skipped for '{0}.{1}' because nested types are not currently supported by the optimizer. Move the scenario to a top-level partial type or disable optimization for this method. |
ContainingType.ContainingType != null |
Method identifier |
| TBDD012 | Warning | Optimization skipped: generic types not supported | TinyBDD optimization was skipped for '{0}.{1}' because generic containing types are not currently supported by the optimizer. Use a non-generic partial type or disable optimization for this method. |
ContainingType.TypeParameters.Length > 0 |
Method identifier |
| TBDD001 (existing) | Warning | TinyBDD optimization failed | Failed to optimize method '{0}': {1} |
Exception during generation | Method location |
Notes
- The new warnings must be emitted instead of generating code when they apply.
- Warnings must be stable (no random ordering; avoid repeated spam where possible).
- Ideally, we should not emit TBDD010 once per method in a class with 200 tests (see “Spam control” below).
3) Spam Control (Recommended)
Add a “warn once per type” strategy for TBDD010:
- When detecting non-partial types, group methods by containing type and emit a single TBDD010 per type.
- Still skip generation for all methods in that type.
Implementation options:
- In the incremental pipeline, project to the containing type symbol key and
.Collect()+.Distinct()to emit one diagnostic. - Or emit per-method but dedupe via incremental grouping (preferred).
4) Tighten Method Detection (Optional but Strongly Recommended)
ContainsBddChain in TinyBddOptimizer is currently string-based (checks for "Given" and "When"/"Then" in method body text). This can cause false positives and unnecessary diagnostics.
Instead:
- Use syntax + semantic checks similar to
OptimizedCodeGenerator.IsBddStartingMethod(...), which already resolves symbols and ensuresGivenis fromTinyBDD*namespace. - This reduces warning noise and reduces “why is TinyBDD yelling at my unrelated async method that happens to contain the word Given”.
5) Docs Update (Required)
Update README / docs to explicitly state:
- Source generation is enabled by default (already stated), and
- Scenarios are optimized only when the containing type is
partial - How to fix:
public partial class MyTests ... - How to opt out:
[DisableOptimization]or[GenerateOptimized(Enabled = false)](as supported today)
Implementation Plan (Concrete)
Step A: Add eligibility checks (type constraints)
In src/TinyBDD.SourceGenerators/TinyBddOptimizer.cs, in Execute(...) before calling OptimizedCodeGenerator:
- Compute
INamedTypeSymbol containingType = method.MethodSymbol.ContainingType; - If
containingType.ContainingType != null→ report TBDD011 and return. - If
containingType.TypeParameters.Length > 0→ report TBDD012 and return. - Determine partial-ness by inspecting all declaring syntaxes:
static bool IsPartial(INamedTypeSymbol type)
{
foreach (var decl in type.DeclaringSyntaxReferences)
{
if (decl.GetSyntax() is TypeDeclarationSyntax tds)
{
if (!tds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
return false;
}
}
return true;
}If !IsPartial(containingType) → report TBDD010 and return.
Only then proceed with OptimizedCodeGenerator.Generate() and context.AddSource(...).
Step B: Add DiagnosticDescriptors (centralize)
Create a small internal diagnostic catalog class in the generator project, e.g.:
src/TinyBDD.SourceGenerators/Diagnostics.cs
Expose DiagnosticDescriptor instances for TBDD010/011/012 and reuse in Execute(...).
Step C: Spam control
Refactor pipeline so we can produce type-level diagnostics once:
- Pipeline 1: Optimizable methods where type is eligible → generate sources.
- Pipeline 2: Optimizable methods where type is ineligible → group by type → emit a single warning per type (TBDD010/011/012 depending on reason; or prefer most specific).
Step D: Update docs
- README: add a “Partial Types Required” section under the source generator docs.
- Include a “Common upgrade warning” snippet showing TBDD010 and the fix.
Test Plan (No Ambiguity)
We need generator-level tests and end-to-end compilation tests.
1) Generator Diagnostics & Output Tests (Recommended: Roslyn test harness)
Add a dedicated test project using Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing (or equivalent) to assert:
Good cases (should generate, no warnings)
- Partial class containing a BDD scenario method → generated source exists.
- Partial record class containing a BDD scenario method → generated source exists.
- Scenario method uses
varin locals and/or lambdas → generator succeeds and output compiles. - Scenario uses expression-bodied method:
public async Task X() => await Given(...).Then(...);→ generates. - Method has parameters and returns Task/ValueTask → generates with correct signature.
Bad cases (should NOT generate; should warn)
6. Non-partial class with a valid BDD chain method:
- Assert no generated sources
- Assert TBDD010 warning at type or method location
-
Nested partial type scenario:
public partial class Outer { public partial class Inner { ... } }- Assert TBDD011
- Assert no generated sources
-
Generic partial type scenario:
public partial class Tests<T> { ... }- Assert TBDD012
- Assert no generated sources
Edge cases (should not crash; should be deterministic)
9. Method uses method-group instead of lambda where generator expects lambda → should not crash; either:
- Report TBDD001 (existing), OR
- Skip generation (if we add new “unsupported pattern” diagnostic later)
- Multiple scenario methods in one non-partial class:
- Assert only one TBDD010 (spam control)
- Method includes
Given/When/Thenidentifiers that are not TinyBDD symbols:
- Ensure no warnings and no generation (requires semantic detection improvement)
2) Integration Tests (Existing TinyBDD.SourceGenerators.Tests)
Extend tests/TinyBDD.SourceGenerators.Tests with:
- A new non-partial test class that looks like the current
SimpleScenarioTestsbut intentionally notpartial.
- Ensure the test project builds and runs.
- Ensure generated output folder does not contain optimized files for that class.
If we can’t assert warnings easily in this style of test, we still keep it as a “compile doesn’t break” guardrail.
Acceptance Criteria
- Upgrading TinyBDD does not introduce new compilation failures for projects whose test classes are not
partial. - For a BDD scenario method in a non-partial containing type, the build succeeds and emits TBDD010 warning (once per type).
- For nested types, the build succeeds and emits TBDD011 warning (once per type).
- For generic types, the build succeeds and emits TBDD012 warning (once per type).
- Existing generator failure diagnostic TBDD001 remains functional and unchanged.
- Generated sources are produced only when eligibility checks pass.
- README/docs explicitly describe the partial requirement and how to resolve warnings.
Extra Follow-ups (Optional, separate issues if needed)
- Add a CodeFix provider: “Make containing type partial” for TBDD010.
- Add a
.editorconfig/ MSBuild property to control severity (e.g., allow users to turn warnings into suggestions):
TINYBDD_SG_DIAGNOSTICS_SEVERITY=TBDD010:suggestion - Replace
ContainsBddChainstring heuristics with semantic detection to reduce noise.
If you want, I can also draft the exact code diff (new Diagnostics.cs, updated pipeline shape, and the minimal set of changes in TinyBddOptimizer.Execute) in a copy/paste-friendly format.