Skip to content

Get ubuntu-latest green, and clear the three codegen follow-ups - #197

Merged
matt-edmondson merged 6 commits into
mainfrom
claude/extract-generalize-codegen-hjt2iy
Aug 27, 2026
Merged

Get ubuntu-latest green, and clear the three codegen follow-ups#197
matt-edmondson merged 6 commits into
mainfrom
claude/extract-generalize-codegen-hjt2iy

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes #191, #193, #194, #195 — everything left over from #182 and #187.

The four land on one branch because that's the branch this work stream uses; they're independent and reviewable in commit order.

#191Test on ubuntu-latest goes green

Red on main since 8b9df02 added Linux to the matrix, with 92 failures, every one in the path tests. The new coverage found what it was added to find on its first run: these tests had only ever run on Windows and had its spelling of everything baked in.

All 92 were test assumptions, not Semantics.Paths defects. The library takes its separators from System.IO.Path and treats a backslash as an ordinary filename character where that is what it is — which is correct. The tests asserted the Windows answer and got the right answer for the wrong platform. IsChildOf_WithMixedSeparators_WorksCorrectly, which the issue singled out as worth reading carefully, is one of these: it failed only because its C:\projects fixture won't construct on Linux.

TestPaths composes fixtures from Path.DirectorySeparatorChar and a platform-appropriate root, so TestPaths.Absolute("projects", "app") is C:\projects\app on Windows and /projects/app elsewhere.

Windows behaviour is unchanged by construction. Rendering every converted fixture back into its Windows spelling reproduces the original string literal exactly — I checked this mechanically across all 14 files, and the only lines that don't round-trip are the ones I deliberately restructured. Worth saying, because a green Linux run proves nothing about the platform these tests came from. CI bears it out: Test on windows-latest is green on every commit here.

Mixed-separator fixtures use Path.AltDirectorySeparatorChar rather than a hard-coded backslash. On Unix both separators are /, so those cases degrade to ordinary paths instead of smuggling in a backslash and quietly testing something else.

Eleven weren't fixture spelling and needed a decision:

Case Decision
UNC (\\server\share) Windows-only concept — [OSCondition(OperatingSystems.Windows)] + [TestCategory("OS-Specific")], matching the existing Parent_OfUncShareRoot_ReturnsTheShareItself
< > : | in names Reserved on Windows, legal in a Unix filename. The validators ask the running platform via Path.GetInvalidFileNameChars, so accepting them off Windows is the correct answer, not a gap. Same gating, reason recorded on each test
"rejects path separators" Now builds the separator from Path instead of hard-coding \, so it asserts the thing it's named for on both platforms rather than one

AbsoluteDirectoryPathParentTests was already platform-correct via a per-platform conditional; TestPaths makes one expression cover both, so the conditional goes.

#193 — SEM001 and SEM003 now say where

SEM004 got a source location in #182; these two were left at Location.None because the collectors that raise them never received the MetadataFile. dimensions.json is thousands of lines and both are typo-catchers, so "which name is wrong" without "where it is written" leaves the reader to do most of the work.

The two want different searches. SEM001 names something that exists nowhere, so the first occurrence in the file is the mistake — the existing unscoped FindLocation is right. SEM003 names dimensions that all exist and are spelled correctly; searching for one unscoped lands on its first legitimate mention somewhere else entirely. Technically a location, practically a wrong answer. So FindLocation gains a scoped overload — find the owning dimension's own "name", then the relationship's "other" at or after it — falling back to the unscoped search and then to Location.None, so an unfamiliar metadata layout degrades to today's behaviour rather than to nothing.

GeneratorDiagnosticTests gains an AssertPointsAt helper that reads the covered text back out of the metadata. That strengthens the existing SEM004 test, which moves onto it: it asserted only that the location wasn't Location.None, which would have passed for a location pointing at the wrong entry — the exact failure mode this is about.

#194 — no more RS2002

Seven warnings on every build, one per SEM diagnostic. The release-tracking analyzer only sees descriptors exposed through DiagnosticAnalyzer.SupportedDiagnostics; these are reported by source generators, which have no such property. Omit them from AnalyzerReleases.Unshipped.md and RS2008 fails the build; include them and RS2002 warns.

Identifiers stay tracked; RS2002 is silenced in Semantics.SourceGenerators.csproj with a comment saying why the rule can't be satisfied here. AnalyzerReleaseTrackingTests already enforces what RS2008 enforces from a vantage point that can see the descriptors. Verified: a clean build of the project reports 0 warnings.

#195 — the Sonar reproduction reaches all six projects

CustomBeforeMicrosoftCommonProps reaches only Semantics.SourceGenerators, the one project using the <Project Sdk="..."> attribute form. The five ktsu.Sdk projects use <Sdk Name="..." /> elements, which that hook doesn't reach. CustomAfterMicrosoftCommonProps does, and is still early enough for restore to pick up the analyzer PackageReference:

dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props

Verified by seeing Sonar findings reported against Semantics.Color and Semantics.Music, neither of which the Before hook reached at all. The "Known limitation" paragraph is replaced with the caveat that actually applies: the globalconfig was calibrated against this repository's profile, so a clean local run is strong evidence rather than proof.

The backlog it exposed

Triaged rather than deferred, since all three were errors under the raised severities:

  • Oklab.Cbrt, S1244 — the exact-zero check is correct: the Newton-Raphson step divides by x * x, and x is zero only when value is, so a tolerance band would send small-but-valid inputs down the shortcut and lose the precision the refinement exists to recover. Suppressed at the method with that justification.
  • NormalizedParameter.Logarithmic, S1244 — same shape, but here it rewrites cleanly: comparing Math.Sign results says "is zero" without comparing floats. No suppression needed.
  • DimensionsGenerator, S3267 — the loop only reads dimension.Name, so it iterates the names.

One finding is mine: threading MetadataFile? through for #193 pushed ResolveForms to eight parameters (S107). The relationship, its file, its owning dimension and its field path all travel together and exist only to let a diagnostic say where it is, so they become one RelationshipSite. ResolveForms drops to four parameters and ReportFormMissing to three.

Test coverage on the new code

The first push cleared CI but failed SonarCloud's gate at 66.7% coverage on new code against a required 80%. Measuring the same thing locally — cobertura output intersected with the lines this branch adds — reproduced it at 76.5% and named the same two files. Every gap was real, so the last commit closes them with tests rather than padding:

  • The eight SEM001 report sites are one per relationship kind per field, each with its own field-path message, and only integrals[...].other had a test. A rename dropping any of the other seven would have failed nothing. Now a DataRow per kind and field, asserting both the location and the field path in the message.
  • SEM003 had only its self branch covered. The other two participants are the interesting ones: a cross product needs the form on its other operand and on its result, and the diagnostic has to name whichever is missing rather than the dimension that owns the relationship.
  • MetadataFile.FindLocation's scoped overload had its happy path covered through SEM003 and none of its fallbacks — which are the whole reason it's safe to use. MetadataFileTests now covers both overloads directly, including a null SourceText.

SonarCloud now reports 100.0% coverage on new code, matching the local measurement of 52/52 lines exactly.

Verification

  • Linux: 1127 total, 0 failed, 1119 passed, 8 skipped — the 8 being the Windows-only cases above. Test on ubuntu-latest green for the first time since the matrix added it.
  • Windows: green throughout.
  • Verify Generated Files: green. git diff over Semantics.Quantities/Generated/ after a rebuild is empty — nothing here changes emitted code.
  • SonarCloud quality gate: passed, 100% coverage on new code, 0 security hotspots, 0% duplication.

Not verified from here: SonarCloud reports 2 new issues alongside the passing gate. sonarcloud.io is unreachable from the agent sandbox (the network policy 403s the CONNECT), so I can't enumerate them, and the local analyzer reproduction doesn't surface them — it reports only the pre-existing Semantics.Test backlog (S4144 on identical test bodies, S101 on SSNString), which this branch doesn't touch. Anyone with dashboard access can see what they are in a click; they didn't block the gate.

claude added 5 commits August 27, 2026 05:27
…ut [patch]

SEM004 was given a source location in #182; SEM001 and SEM003 were left
reporting at Location.None because the collectors that raise them never
received the MetadataFile. dimensions.json is thousands of lines, and both are
typo-catchers, so "which name is wrong" without "where it is written" leaves
the reader to do most of the work themselves.

Thread MetadataFile? through CollectAllOperators/CollectAllProducts and their
four collectors, and through ResolveForms, then report through ReportAt.

The two diagnostics want different searches, which is why this is not one call:

  SEM001 names something that does not exist anywhere, so the first occurrence
  of it in the file is the mistake. The existing unscoped FindLocation is
  exactly right.

  SEM003 names dimensions that all exist and are spelled correctly. Searching
  for one unscoped lands on its first legitimate mention, which is somewhere
  else entirely — technically a location, practically a wrong answer. So
  FindLocation gains a scoped overload: find the owning dimension's own "name"
  property, then find the relationship's "other" at or after it. It falls back
  to the unscoped search, and then to Location.None, so a metadata layout it
  does not recognise degrades to what was there before rather than to nothing.

GeneratorDiagnosticTests gains an AssertPointsAt helper that reads the covered
text back out of the metadata, and location tests for SEM001 and SEM003 built
on it. Sem004_PointsAtWhereTheUnitIsWrittenRatherThanAtNothing moves onto the
same helper, which strengthens it: it asserted only that the location was not
Location.None, which would also have passed for a location pointing at the
wrong entry — the failure mode this change is specifically about.

Closes #193

Co-Authored-By: Claude <noreply@anthropic.com>
Every build emitted one RS2002 per SEM diagnostic — seven of them since #182
added SEM006 and SEM007.

The release-tracking analyzer only sees descriptors exposed through
DiagnosticAnalyzer.SupportedDiagnostics. These are reported by source
generators, which have no such property, so it can never see them however they
are declared. That left the project stuck between its own rules: omit the
identifiers from AnalyzerReleases.Unshipped.md and RS2008 fails the build;
include them and RS2002 warns because nothing claims them.

Keep the identifiers tracked and silence RS2002, scoped to
Semantics.SourceGenerators.csproj with a comment saying why the rule cannot be
satisfied here. AnalyzerReleaseTrackingTests already enforces what RS2008
enforces, from a vantage point that can actually see the descriptors, so the
discipline survives the suppression.

Verified: a clean build of the project now reports 0 warnings.

Closes #194

Co-Authored-By: Claude <noreply@anthropic.com>
CLAUDE.md documented the reproduction along with a limitation: it reached only
Semantics.SourceGenerators, the one project declaring its SDK with the
<Project Sdk="..."> attribute form. The five ktsu.Sdk projects use <Project>
with <Sdk Name="..." /> elements, which CustomBeforeMicrosoftCommonProps does
not reach — so five of six projects had no local coverage and their findings
only surfaced after a push.

CustomAfterMicrosoftCommonProps does reach that form, and is still early
enough for restore to pick up the analyzer PackageReference. Switch the
documented invocation and the comment in sonar-local.props, and replace the
limitation paragraph with the caveat that actually applies: the globalconfig
was calibrated against this repository's profile, so a clean local run is
strong evidence rather than proof.

Verified by building with the After hook and seeing Sonar findings reported
against Semantics.Color and Semantics.Music, neither of which the Before hook
reached at all.

The first wider run surfaced one finding, in Semantics.Color: S1244 against
the exact zero check in Oklab.Cbrt. The comparison is correct — the
Newton-Raphson step divides by x * x, and x is exactly zero only when value
is, so a tolerance band would send small-but-valid inputs down the shortcut
and lose the precision the refinement exists to recover. Suppressed at the
method with that justification rather than reworked.

Closes #195

Co-Authored-By: Claude <noreply@anthropic.com>
`Test on ubuntu-latest` has been red on main since 8b9df02 added Linux to the
matrix, with 92 failures — every one in the path tests. The new coverage found
what it was added to find on its first run: these tests had only ever been
exercised on Windows, and had its spelling of everything baked in.

All 92 turned out to be test assumptions rather than Semantics.Paths defects.
The library takes its separators from System.IO.Path and treats a backslash as
an ordinary filename character where that is what it is, which is correct; the
tests asserted the Windows answer and got the right one for the wrong platform.

TestPaths composes fixtures from Path.DirectorySeparatorChar and a
platform-appropriate root, so `TestPaths.Absolute("projects", "app")` is
`C:\projects\app` on Windows and `/projects/app` elsewhere. Rendering every
converted fixture back into its Windows spelling reproduces the original string
literal exactly, so Windows behaviour is unchanged by construction — worth
stating because a Linux-green suite proves nothing about the platform the tests
came from.

Mixed-separator fixtures use Path.AltDirectorySeparatorChar rather than a
hard-coded backslash. On Unix both separators are '/', so those cases degrade to
ordinary paths rather than smuggling in a backslash and testing something else.

Eleven were not fixture spelling and needed a decision:

  - UNC is a Windows concept; \\server\share is an ordinary relative filename on
    Unix. [OSCondition(OperatingSystems.Windows)] + [TestCategory("OS-Specific")],
    matching Parent_OfUncShareRoot_ReturnsTheShareItself.
  - < > : | are reserved on Windows and legal in a Unix filename. The validators
    ask the running platform via Path.GetInvalidFileNameChars, so accepting them
    off Windows is the correct answer, not a gap. Same gating, with the reason on
    each test.
  - "rejects path separators" tests now build the separator from Path rather than
    hard-coding a backslash, which makes them assert the thing they are named
    for on both platforms instead of only one.

AbsoluteDirectoryPathParentTests was already platform-correct via a per-platform
conditional; TestPaths makes one expression cover both, so the conditional goes.

Result on Linux: 1110 total, 0 failed, 1102 passed, 8 skipped — the 8 being the
Windows-only cases above.

Closes #191

Co-Authored-By: Claude <noreply@anthropic.com>
…patch]

Three findings, two of them only visible because the local reproduction now
reaches every project.

S107 on ResolveForms is mine: threading MetadataFile? through for #193 pushed it
to eight parameters. The relationship, the file it came from, the dimension that
owns it and its field path all travel together and only exist to let a
diagnostic say where it is, so they become one RelationshipSite. ResolveForms
drops to four parameters and ReportFormMissing to three, and the call sites read
better than the seven-argument version they replace.

S1244 on NormalizedParameter.Logarithmic is pre-existing, newly visible. The
exact zero check is correct — a logarithmic range is undefined at zero but
perfectly well defined at 1e-9, so a tolerance band would reject legitimate
ranges. Comparing Math.Sign results says the same thing without comparing
floats, so this needs no suppression.

S3267 on DimensionsGenerator is pre-existing: the loop only ever reads
dimension.Name, so it iterates the names.

Verified: the whole solution now builds clean under the Sonar analyzers except
for a pre-existing backlog in Semantics.Test (S4144 on identical test bodies,
S101 on SSNString), which is untouched by this branch and is its own triage.

Generated output is unchanged — verified by rebuilding Semantics.Quantities and
diffing Generated/.

Co-Authored-By: Claude <noreply@anthropic.com>
@matt-edmondson matt-edmondson changed the title Diagnostic locations for SEM001/SEM003, no more RS2002, and a Sonar reproduction that reaches every project Get ubuntu-latest green, and clear the three codegen follow-ups Aug 27, 2026
…ed [patch]

SonarCloud failed the quality gate on #197: 66.7% coverage on new code against
a required 80%. Measuring the same thing locally (cobertura, intersected with
the lines the branch adds) put it at 76.5% and named the same two files.

Every gap was a real one, and the tests are real rather than coverage padding:

QuantitiesGenerator — the eight SEM001 report sites are one per relationship
kind per field, each with its own field-path message, and only
integrals[...].other had a test. A rename that dropped any of the other seven
would not have failed anything. Now a DataRow per kind and field, asserting both
the location and the field path in the message.

SEM003 had only its self branch covered. The other two participants are the
interesting ones — a cross product needs the form on its other operand and on
its result, and the diagnostic has to name whichever is missing it rather than
the dimension that owns the relationship. Two tests, using a second dimension
that declares vector0 only.

MetadataFile — the scoped FindLocation overload had its happy path covered
through SEM003 and none of its fallbacks, which are the whole reason it is safe
to use: an unrecognised metadata layout has to degrade to the unscoped search
and then to Location.None rather than throw or point somewhere arbitrary.
MetadataFileTests covers both overloads directly, including a null SourceText.

New-code coverage measured the same way afterwards: 52/52 lines, 100%.
1127 tests, 0 failures. The Sonar analyzers report nothing new — the remaining
findings are the pre-existing Semantics.Test backlog (S4144, S101).

Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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.

92 Semantics.Paths tests fail on Linux, and main has been red since the matrix added it

2 participants