Skip to content

Fix/union accessor declared once - #158

Merged
kieronlanning merged 12 commits into
mainfrom
fix/union-accessor-declared-once
Nov 15, 2026
Merged

kieronlanning merged 12 commits into
mainfrom
fix/union-accessor-declared-once

Conversation

@kieronlanning

Copy link
Copy Markdown
Contributor

No description provided.

Restored by the skills CLI: the updated purview-buildsdk-* and

purview-sourcegenerator-* skills, the new purview-site-* and catalogue skills, and

the matching skills-lock.json entries. Committed on its own, as AGENTS.md requires

for .agents/skills/**.
Every target emitter emitted the UnwrapUnion_<Type> accessor into its own
hint file. Activity, Logging and Metric are one partial class, so a union
consumed by two target families was declared twice and the consumer failed
with CS0111, then CS0121 at the now-ambiguous call sites. 5.2.1 unwrapped
inline, so this regressed in 6.0.0.

The accessor is a shared member like the dependency-injection extension and
the constructor, so it now follows the same ownership rule: the canonical
target (Activities, else Logging, else Metrics) declares it once per union
type and the other files call it.

UnionAccessorPlanner decides the list:

- one registered target: the target's own unions, so a single-target
  interface's output is unchanged and no consumer needs a change;
- more than one: the interface's unions in declaration order, ignoring
  parameter [ExcludeTargets]. A target never sees a method that belongs to
  another family only ([Logger][Meter] with a union on the [Counter]
  method), and an Activities-excluded parameter is still unwrapped by the
  log or metric call site.

Also fixes [Event] tags recording the union struct instead of its case
value: event tags are collected into an ActivityTagsCollection rather than
set through SetTag, and that was the one call site still using the raw
parameter.

Regression tests (red on 6.0.0 with the reported ids) cover both reported
interfaces, the six-shape emission matrix, the [Logger][Meter] ownership
case, single-target placement and no new TSG diagnostics. The packed Native
AOT and Blazor consumers carry the shape end to end, with warnings as
errors. Verified against the reporter's solution: ChangeOps builds on 6.0.1
with one declaration per union and no CS0111.
The planner computes the accessor list from the interface rather than from
the owning target's own method records, so the value it puts on the target
model has to stay value-equatable or every edit anywhere would re-run
generation.

Adds two incremental tests over a multi-target interface with a union both
families consume:

- unchanged sources: every framework-named stage output is Cached or
  Unchanged on the second run, which is what proves the new
  EquatableArray<UnionTargetRecord> member compares equal;
- an unrelated type added: the stages stay cached and the generated sources
  are byte-identical, which proves the interface-wide scan does not widen
  invalidation.

The union source is hand-written with plain class cases so the net48 leg
compiles it too, and the tests therefore run on all three legs.
The root Justfile now imports one barrel, .build/just/index.just, and holds only what
belongs to this repository: its variables, its own recipes and three deliberate overrides
(test, pack, pipeline-pack-validate). Everything a shared module provides was deleted, so a
leftover copy cannot silently replace the module's recipe.

.lint -> lint-check and ensure-pipeline-tool -> ensure-tools come from the modules. bump is
new: it rewrites package.json and then runs update-version through _after-bump, so a release
needs only the bump. The modules are copied verbatim from the purview-buildsdk-justfile skill
and are byte-identical to its assets.
24 pages sat in one flat folder, so the sidebar was a list of 24 links and the URL of every
page was one segment. They now live in guide/, telemetry/, reference/, migration/ and
contributing/, with Home, Getting-Started and Generation left at the root: the catalogue names
Getting-Started as its rootPage and deep-links generation from a use case.

_redirects.yml records all 21 moves, so a bookmark or a link from another repository still
resolves; the sidebar groups the pages in reading order, and 148 relative links were rewritten
to resolve from their page's new folder. Every inbound link follows: README.md and the samples'
README link the canonical purview.dev URLs rather than relying on the redirect, and the
diagnostic links in AnalyzerReleases.Unshipped.md, the source comments and the benchmark skill
name the new paths.

The ASCII release-flow diagram (it appeared twice) became a LikeC4 model paired with a Mermaid
fence, the two unlabelled fences are labelled, and the four pages that opened straight into a
callout now open with the paragraph the site uses as its meta description.
The nullable-union accessor declared its local as raw text through two Write calls. The
framework's PSGFR19 rejects that shape ('Write' with a statement should use 'Assignment'), so
the generator did not build against Purview.SourceGeneratorFramework 2.1.0.

The typed Assignment overload emits the same line. Verified against the pinned 2.0.0 as well,
since the SDK bump is not part of this change.

The caching suite also gains the three cases it was missing: deleting an interface removes its
generated source, renaming replaces the hint name, and a changed global option re-resolves the
generation context instead of leaving it cached.
Roughly 150 of the 531 tests carried no Arrange/Act/Assert markers at all, concentrated in the
refactoring suites, and five tests marked the act and the arrangement with one combined
'Arrange / Act' line.

Every braced test now carries // Act, and // Assert wherever the assertion is in its own body.
The four snapshot refactoring suites stop at // Act: their assertion is the snapshot comparison
inside CodeRefactoringTestBase. // Arrange appears where there is something to arrange; the
tests whose first statement is the act are left without an empty section. The eight
expression-bodied delegates in RegexValidationTests are unchanged - their AAA lives in the
shared MatchAsync helper.
Purview.BuildSdk 2.1.0 (global.json) rewrites its managed .editorconfig, which is why that file
carries the new version marker; Purview.SourceGeneratorFramework and Purview.Skills.Testing
move to 2.1.0, and the purview.build local tool to 0.9.1.

Every one of these is a lower bound for the package this repository publishes, so the floor
proved by just test-floor moves with them.
Collection expressions replace the List/copy pair in TypeParameterHelpers, the tag-provider local
function becomes expression-bodied, and the union and shared helpers gain the comments the new
rules look for: which union shapes read a case without boxing, that the assembly version has no
pre-release tag, and that a non-canonical target emits no accessors.

Generated output is unchanged; the full suite passes on both 2.0.0 and 2.1.0.
The leak and clean-output fixtures embed JSON, so each raw string carries the lang=json,strict
prompt, and the factories that capture nothing become static local functions.
The hook ran dotnet csharpier directly; just lint-fix is the same formatter behind the recipe the
docs name, and bun x replaces bunx.
@kieronlanning
kieronlanning enabled auto-merge (squash) November 15, 2026 00:36
@kieronlanning
kieronlanning merged commit 31a0ad8 into main Nov 15, 2026
5 checks passed
@kieronlanning
kieronlanning deleted the fix/union-accessor-declared-once branch November 15, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant