Skip to content

Read a specification step that names a value the specification holds - #2451

Open
woksin wants to merge 3 commits into
mainfrom
feat/screenplay-specification-held-values
Open

Read a specification step that names a value the specification holds#2451
woksin wants to merge 3 commits into
mainfrom
feat/screenplay-specification-held-values

Conversation

@woksin

@woksin woksin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Changed

Fixed

A step states what it is about by constructing it, and a specification
routinely puts that construction in a member and names the member in the
step instead - the same event is stated once and asserted on later, or
the command is built where the values it needs already are. Reading only
what is written inline left every such scenario out whole.

The member is followed one hop, to the single place it was put together.
Given in two places, or given under a condition, it held different values
in different runs and the source does not say which one the step saw - so
it stays unread and the scenario is left out with SP0039, the same as any
other conditional step. A '= null!' declaration is not one of those
places; it satisfies the compiler and states no value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JfEAq3cyHk3mrWpo8QA9db
@woksin

woksin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer notes.

What changed. SpecificationStepReader required both a given-state and the issued command to be a BaseObjectCreationExpressionSyntax written at the step. New HeldValues follows an IdentifierNameSyntax / MemberAccessExpressionSyntax resolving to a field, property or local one hop to the single place it was given a value, and returns that construction with the semantic model of its tree (HeldConstruction) — the two need not share a tree once the member is inherited.

The discipline, deliberately kept narrow. Following is refused when:

  • the value is given in more than one place (declaration initializer + assignment, or two assignments);
  • the assignment sits under anything StepsTaken.IsConditional recognises — if, switch, a loop, a ternary, a lambda;
  • what it is given is not itself a construction (no chains — one hop, never two).

All three keep reporting SP0039 and take the scenario with them, which is the existing rule.

One non-obvious carve-out. RegisterAuthor _command = null!; is the idiomatic declaration for a member filled in later, so counting it as a place the value is given would make every held value look like it was given twice — the feature would recover nothing. StatesNothing excludes null, null!, default and default!.

Spec churn worth looking at. a_specification_whose_steps_cannot_be_read had a case named and_the_command_is_held_in_a_field asserting exactly the behaviour this PR changes. It is now and_the_command_is_held_in_a_field_given_twice — a field given a value in two places — so the file still covers three genuinely unreadable shapes and the count assertion is unchanged. The readable shape moved to the new a_specification_holding_what_its_steps_state.

Verification. Screenplay.Specs 1181 passed / 0 failed. Debug and Release builds zero warnings. markdownlint-cli2 clean on the changed page.

Not verified. The issue's estimate of ~77 recovered scenarios on the reference application — I have no checkout of it, so that number is unconfirmed. Worth measuring before closing #2412 if the count matters.

WithDefaults ran twice on the way through a generation - once in the
generator against the assembly being analyzed, once again in the emitter
against the domain of the model - and the two fallbacks are different
questions that only happen to agree today. A host calling Emit directly
therefore behaved subtly differently from the same model reached through
a generation.

Resolved options now answer with themselves, so whichever entry point
sees them first decides the fallback and the other half reads that
decision rather than working one out again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JfEAq3cyHk3mrWpo8QA9db
Both change the Screenplay generator and this repository ships related
work as one pull request; keeping them apart would split one release
note across two PRs carrying different semver labels.
@woksin

woksin commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

CI note. A red specs (Source/DotNET/MongoDB.Specs) run may appear in this PR's history. It is an unrelated pre-existing flake — same SHA, no changes, green on re-run — now filed as #2453. This branch changes only Source/DotNET/Screenplay/, and MongoDB.Specs has no dependency path to it.

Consolidation. #2452 (Screenplay options resolved once, #2401 item 7) was folded in here rather than shipped separately, since both change the Screenplay generator and this repository ships related work as one PR. Its commit f6dbd59e is unchanged; the label stays minor, which dominates that change's patch.

Verification of the merged branch. Screenplay.Specs 1181 passed / 0 failed. Release build zero warnings.

@woksin

woksin commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: #2522 just merged and it touches Source/DotNET/Screenplay/Analysis/Specifications/SpecificationStepReader.cs, which this PR also changes. Expect a conflict.

More important than the conflict is why that file changed. SpecificationStepReader was one of several places calling compilation.GetSemanticModel(tree) directly. That throws SyntaxTree is not part of the compilation whenever the tree belongs to a referenced project's compilation — which happens for any multi-project application, because MSBuildWorkspace wires project references as CompilationReferences and their symbols keep real DeclaringSyntaxReferences. It took Screenplay generation down completely on real applications (#2521).

The fix routes those reads through Analysis/SemanticModels.csmodels.For(tree) finds the compilation that actually holds the tree and returns null when none does, so an unreadable value degrades to a diagnostic instead of an exception.

When resolving the conflict, please keep the SemanticModels routing. Reinstating a direct compilation.GetSemanticModel(...) in this reader brings the crash back, and no spec will catch it: every spec in Screenplay.Specs builds its compilation from source strings, so a second compilation never exists. #2522 adds coverage that does exercise the boundary — worth rebasing onto it rather than merging around it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant