From c9373dd00c11b6a498d55c357455fd175a85a9eb Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 29 Aug 2026 23:30:41 +0000 Subject: [PATCH 01/15] Make the code-fence gate falsifiable, and refuse an empty corpus check-code-fence-syntax.sh was the one linter left on the missing-red-half work list: nothing under scripts/tests/ named it, so a green Repo Lint was evidence about docs/ and no evidence the gate still reports. Writing its self-test found a second defect. The gate counted issues but never counted files, so a corpus holding no markdown -- docs/ renamed, a tree moved, a find that stopped matching -- printed "No code fence syntax issues found" and exited 0. That is the exact shape test-empty-corpus- gates.ps1 exists to prevent, and this gate had never been in that family. It now reports its scan count and fails on an empty walk. The self-test carries a red half per rule the gate enforces: backtick and tilde fences, indented fences, fences longer than three backticks, a file in a subdirectory, a multi-offence count, a missing directory and an empty corpus. Verified falsifiable against three mutants -- suppressing the issue counter fails six cases, removing the empty-corpus guard fails one, and -maxdepth 1 fails the recursion case. missingRedHalf is empty again; no linter in scripts/ is unfalsifiable. Fixes #604 Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 1 + scripts/check-code-fence-syntax.sh | 20 +- scripts/run-contract-tests.js | 5 + scripts/tests/test-check-code-fence-syntax.sh | 239 ++++++++++++++++++ .../test-check-code-fence-syntax.sh.meta | 7 + scripts/tests/test-run-repo-lint.js | 12 +- 6 files changed, 276 insertions(+), 8 deletions(-) create mode 100755 scripts/tests/test-check-code-fence-syntax.sh create mode 100644 scripts/tests/test-check-code-fence-syntax.sh.meta diff --git a/package.json b/package.json index dd363bb96..ff5456cb2 100644 --- a/package.json +++ b/package.json @@ -202,6 +202,7 @@ "test:validate-hook-permissions": "bash scripts/tests/test-validate-hook-permissions.sh", "test:validate-hook-sync-calls": "pwsh -NoProfile -File scripts/tests/test-validate-hook-sync-calls.ps1", "test:validate-github-pages-css": "bash scripts/tests/test-validate-github-pages-css.sh", + "test:check-code-fence-syntax": "bash scripts/tests/test-check-code-fence-syntax.sh", "test:lint-dependabot": "pwsh -NoProfile -File scripts/tests/test-lint-dependabot.ps1 -VerboseOutput", "test:lint-duplicate-usings": "pwsh -NoProfile -File scripts/tests/test-lint-duplicate-usings.ps1 -VerboseOutput", "test:lint-preserve-attributes": "pwsh -NoProfile -File scripts/tests/test-lint-preserve-attributes.ps1 -VerboseOutput", diff --git a/scripts/check-code-fence-syntax.sh b/scripts/check-code-fence-syntax.sh index 2d78aec1d..c3d036d28 100755 --- a/scripts/check-code-fence-syntax.sh +++ b/scripts/check-code-fence-syntax.sh @@ -49,6 +49,10 @@ echo "" # Array to store issues for summary declare -a ISSUE_LIST +# A scan that matched nothing is the absence of a measurement, not a pass (#556): docs/ renamed, +# a corpus moved, or a find that stopped matching all report "no issues found" otherwise. +SCANNED=0 + # Find all markdown files and check for invalid code fence syntax # Pattern matches code fences with language followed by comma and attributes # Examples of invalid patterns: @@ -56,6 +60,7 @@ declare -a ISSUE_LIST # ```rust,no_run # ```python,something while IFS= read -r -d '' mdfile; do + SCANNED=$((SCANNED + 1)) line_num=0 while IFS= read -r line || [[ -n "$line" ]]; do @@ -80,12 +85,25 @@ while IFS= read -r -d '' mdfile; do ISSUES=$((ISSUES + 1)) fi done < "$mdfile" -done < <(find "$DOCS_DIR" -name "*.md" -type f -print0 2>/dev/null) +done < <(find "$DOCS_DIR" -name "*.md" -type f -print0) echo "----------------------------------------" echo "Summary" echo "----------------------------------------" +if [ "$SCANNED" -eq 0 ]; then + printf "${RED}ERROR: No markdown files found under %s${NC}\n" "$DOCS_DIR" + echo "" + echo "The corpus is empty, so this run checked nothing. Point the validator at a" + echo "directory that contains markdown, or fix the path if the docs tree moved." + echo "" + printf "${RED}VALIDATION FAILED${NC}\n" + exit 1 +fi + +printf "Markdown files scanned: ${BLUE}%d${NC}\n" "$SCANNED" +echo "" + if [ "$ISSUES" -eq 0 ]; then printf "${GREEN}No code fence syntax issues found.${NC}\n" echo "" diff --git a/scripts/run-contract-tests.js b/scripts/run-contract-tests.js index 3744b9a71..5372eaea7 100644 --- a/scripts/run-contract-tests.js +++ b/scripts/run-contract-tests.js @@ -106,6 +106,11 @@ const CHECKS = [ name: "GitHub Pages CSS validator self-test", run: "npm run test:validate-github-pages-css" }, + { + id: "check-code-fence-syntax", + name: "Code fence syntax gate self-test", + run: "npm run test:check-code-fence-syntax" + }, { id: "lint-dependabot", name: "Dependabot linter self-test", diff --git a/scripts/tests/test-check-code-fence-syntax.sh b/scripts/tests/test-check-code-fence-syntax.sh new file mode 100755 index 000000000..9853aca92 --- /dev/null +++ b/scripts/tests/test-check-code-fence-syntax.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +# ============================================================================= +# Self-test for scripts/check-code-fence-syntax.sh +# ============================================================================= +# The gate scans a markdown corpus for code fences carrying comma-separated +# attributes (```csharp,ignore), which MkDocs renders as an unknown language +# rather than as C#. Run against the repository's own docs/ it prints +# "VALIDATION PASSED", which is evidence about docs/ and no evidence at all +# that the gate still reports (#556, #604). +# +# It already takes the corpus directory as $1, so no production change was +# needed to make it testable. +# +# Green half: +# - the repository's real docs/ passes +# - fences that are legal stay legal: no language, a language alone, and +# space-separated attributes, which MkDocs does support +# - a comma inside prose or inside a fenced body is not a fence attribute +# +# Red halves, one per way the gate must report, each asserted on the message +# for that specific reason so a fixture tripping a neighbouring rule cannot +# read as covering the one it is named for: +# - a corpus directory that does not exist +# - a corpus that exists but holds no markdown -- an empty scan is the +# absence of a measurement, not a pass +# - a backtick fence with a comma attribute +# - a tilde fence with a comma attribute +# - an indented fence +# - a fence longer than three backticks +# - a file in a subdirectory, so a lost recursion goes red +# - two offences in one corpus are both counted +# - the report names the offending file, line and replacement +# ============================================================================= + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +GATE="$REPO_ROOT/scripts/check-code-fence-syntax.sh" +REAL_DOCS="$REPO_ROOT/docs" + +WORKSPACE="$(mktemp -d)" +trap 'rm -rf "$WORKSPACE"' EXIT + +PASSED=0 +FAILED=0 +FAILED_NAMES=() + +pass() { + echo " [PASS] $1" + PASSED=$((PASSED + 1)) +} + +fail() { + echo " [FAIL] $1" + echo " $2" + FAILED=$((FAILED + 1)) + FAILED_NAMES+=("$1") +} + +# Each corpus is a fresh directory holding one markdown file, so a red half +# names exactly one offence and cannot borrow a neighbour's. +make_corpus() { + local name="$1" + local relative="$2" + local corpus="$WORKSPACE/$name" + mkdir -p "$corpus/$(dirname "$relative")" + cat > "$corpus/$relative" + printf '%s' "$corpus" +} + +run_gate() { + GATE_OUTPUT="$(bash "$GATE" "$1" 2>&1)" + GATE_EXIT=$? + return 0 +} + +expect_pass() { + local name="$1" + local corpus="$2" + run_gate "$corpus" + if [ "$GATE_EXIT" -ne 0 ]; then + fail "$name" "gate rejected a corpus it must accept (exit $GATE_EXIT): $GATE_OUTPUT" + return + fi + pass "$name" +} + +expect_fail() { + local name="$1" + local corpus="$2" + local expected="$3" + run_gate "$corpus" + if [ "$GATE_EXIT" -eq 0 ]; then + fail "$name" "gate accepted a corpus it must reject: $GATE_OUTPUT" + return + fi + if ! printf '%s' "$GATE_OUTPUT" | grep -qF -- "$expected"; then + fail "$name" "rejected, but not for the reason under test. Expected to contain '$expected'. Got: $GATE_OUTPUT" + return + fi + pass "$name" +} + +echo "" +echo "Running check-code-fence-syntax.sh self-tests" +echo "" + +# -- Green half -------------------------------------------------------------- +expect_pass "the repository docs/ passes" "$REAL_DOCS" + +# The count is the empty-corpus guard's green half: a run that reports a number +# is a run that walked a corpus, which "no issues found" alone cannot show. +run_gate "$REAL_DOCS" +if printf '%s' "$GATE_OUTPUT" | grep -qE 'Markdown files scanned: .*[1-9]'; then + pass "a passing run reports how many files it scanned" +else + fail "a passing run reports how many files it scanned" "expected a non-zero scan count, got: $GATE_OUTPUT" +fi + +LEGAL="$(make_corpus legal ok.md <<'MARKDOWN' +# Legal fences + +```csharp +int value = 1; +``` + +``` +no language at all +``` + +```csharp title="Example.cs" hl_lines="1 2" +int spaced = 2; +``` + +~~~python +value = 3 +~~~ + +Prose may say ```csharp,ignore``` is wrong without being wrong itself, and a +fenced body may contain commas: + +```text +one, two, three +``` +MARKDOWN +)" +expect_pass "legal fences, spaced attributes and prose commas pass" "$LEGAL" + +# -- Red halves -------------------------------------------------------------- +expect_fail "a corpus directory that does not exist is rejected" \ + "$WORKSPACE/absent" "Docs directory not found" + +mkdir -p "$WORKSPACE/no-markdown/nested" +printf 'not markdown\n' > "$WORKSPACE/no-markdown/nested/readme.txt" +expect_fail "a corpus with no markdown is rejected" \ + "$WORKSPACE/no-markdown" "No markdown files found" + +BACKTICK="$(make_corpus backtick guide.md <<'MARKDOWN' +# Guide + +```csharp,ignore +int value = 1; +``` +MARKDOWN +)" +expect_fail "a backtick fence with a comma attribute is rejected" "$BACKTICK" "VALIDATION FAILED" + +run_gate "$BACKTICK" +for fragment in "guide.md:3" '```csharp,ignore' "(remove ',ignore')"; do + if printf '%s' "$GATE_OUTPUT" | grep -qF -- "$fragment"; then + pass "the report names $fragment" + else + fail "the report names $fragment" "not present in: $GATE_OUTPUT" + fi +done + +TILDE="$(make_corpus tilde guide.md <<'MARKDOWN' +~~~python,no_run +value = 1 +~~~ +MARKDOWN +)" +expect_fail "a tilde fence with a comma attribute is rejected" "$TILDE" '```python,no_run' + +INDENTED="$(make_corpus indented guide.md <<'MARKDOWN' +1. A step: + + ```rust,no_run + let value = 1; + ``` +MARKDOWN +)" +expect_fail "an indented fence is rejected" "$INDENTED" '```rust,no_run' + +LONG_FENCE="$(make_corpus long-fence guide.md <<'MARKDOWN' +````markdown,linenums +```csharp +int value = 1; +``` +```` +MARKDOWN +)" +expect_fail "a fence longer than three backticks is rejected" "$LONG_FENCE" '```markdown,linenums' + +NESTED="$(make_corpus nested features/deep/guide.md <<'MARKDOWN' +```csharp,ignore +int value = 1; +``` +MARKDOWN +)" +expect_fail "a file in a subdirectory is scanned" "$NESTED" "features/deep/guide.md:1" + +MULTIPLE="$(make_corpus multiple guide.md <<'MARKDOWN' +```csharp,ignore +int value = 1; +``` + +```rust,no_run +let value = 1; +``` +MARKDOWN +)" +expect_fail "two offences in one corpus are both counted" "$MULTIPLE" "Found 2 code fence syntax issue(s)" + +echo "" +echo "Passed: $PASSED" +echo "Failed: $FAILED" + +if [ "$FAILED" -gt 0 ]; then + echo "" + echo "Failed tests:" + for name in "${FAILED_NAMES[@]}"; do + echo " - $name" + done + exit 1 +fi + +exit 0 diff --git a/scripts/tests/test-check-code-fence-syntax.sh.meta b/scripts/tests/test-check-code-fence-syntax.sh.meta new file mode 100644 index 000000000..d20771042 --- /dev/null +++ b/scripts/tests/test-check-code-fence-syntax.sh.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 37ee99d6eb496972d86f1d1126b78795 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/scripts/tests/test-run-repo-lint.js b/scripts/tests/test-run-repo-lint.js index 14462413b..6bd4102d1 100644 --- a/scripts/tests/test-run-repo-lint.js +++ b/scripts/tests/test-run-repo-lint.js @@ -545,13 +545,11 @@ runTest("no linter in scripts/ has been left unfalsifiable", () => { // map stays, because the two assertions below it are the mechanism that keeps it a work list -- // an entry may not outlive its file, and may not outlive its coverage. // - // Refilled by widening the family below to `check-*`: the rule had never been applied to a - // `check-` gate at all, and one of them has no self-test. This entry is parked on #600, the issue - // whose review surfaced it, and wants an issue of its own -- closing it means adding - // scripts/tests/test-check-code-fence-syntax.sh with a malformed-fence fixture the checker must - // report, and registering it in scripts/run-contract-tests.js so the reachability half below is - // satisfied. - const missingRedHalf = new Map([["scripts/check-code-fence-syntax.sh", "#600"]]); + // Refilled by widening the family below to `check-*`, then emptied again in session 237: + // check-code-fence-syntax.sh was the one `check-` gate with no self-test, and #604 gave it + // scripts/tests/test-check-code-fence-syntax.sh -- which also found the gate reporting a clean + // pass over a corpus holding no markdown at all. + const missingRedHalf = new Map(); // This file and its sibling are REGISTRIES: they name linters in allowlists rather than run // them, so scanning them for a mention counts an excuse as coverage. The first draft did, and From 8ef2574242c7069b79e85e01e0c1b275272f76c5 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sat, 29 Aug 2026 23:56:29 +0000 Subject: [PATCH 02/15] Record a hand-written subtype number so deleting it retires it A subtype's field number is a durable wire contract: a payload resolves a subtype by that number and nothing else. For the explicit forms -- [WProtoSubtype(typeof(Weapon), 100)] and [WProtoInclude(100, typeof(Melee))] -- the declaration was the only record that 100 had ever been spent, and it is deleted along with the type it sits on. WPROTO039 fires on two LIVE claims and has no memory, so a number freed by a deletion was indistinguishable from one never used: the next subtype added could be handed 100, and every payload written by an older build read that field back as the wrong type. No diagnostic, no exception. The planner now records an explicitly numbered declaration beside the assigned ones, which is what turns its deletion into a retirement, and the generator refuses any declaration -- subtype or include -- claiming a number the assembly has retired, naming the type that held it. The refusal is by NAME, so re-adding the deleted type still restores its own number. Two more of the same class, found while writing the tests: - Deleting the number from an attribute made the pair look brand new, so it was handed the smallest free number instead of keeping the one it wrote. Editing a number in place left no trace at all. Both now record the new number and retire what they left. - A pair that had retired two numbers -- what a hand-edited number leaves behind -- lost one of them on the next run, because re-emission was keyed by pair rather than by pair and number. A dropped retirement is a number that is free again a run later. Nine new plan and generator tests, red before the change. The three existing tests that asserted the old behaviour are updated in place: the "no manifest at all" guard now rests on FreshlyAssigned, which is the condition the unattended pass actually reads, so adopting the package still writes nothing into a project that invents no numbers. 689 generator tests pass against protobuf-net 3.2.56 and 688 against 2.4.9. The shipped analyzer is rebuilt and byte-identical to its sources. Fixes #606 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- Editor/Tools/WProtoSubtypeTagPlan.cs | 90 +++++- .../SubtypeTagManifestTests.cs | 268 +++++++++++++++++- .../SubtypeMap.cs | 37 +++ .../SubtypeTagManifest.cs | 65 ++++- .../WProtoGenerator.cs | 10 + ...opStudios.UnityHelpers.Proto.Generator.dll | Bin 193536 -> 194560 bytes Tests/Runtime/WProtoSubtypeTags.cs | 10 +- docs/features/serialization/serialization.md | 6 + 9 files changed, 455 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 556f85e4b..6865b663e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add an editor validation engine: implement `IValidationRule` and run it across the whole project a few milliseconds per editor tick instead of freezing for thirty seconds. Only the assets a rule claims are loaded. See [Asset Validation](./docs/features/editor-tools/asset-validation.md) ([#288](https://github.com/Ambiguous-Interactive/unity-helpers/issues/288)). -- Add `[WProtoSubtype(typeof(Base))]`, so a subtype joins a WallstopProto hierarchy without picking a field number. The editor assigns and commits the number after the reload that first sees it, and **Assign WallstopProto Subtype Tags** retires a removed one so it is never reused. See [Polymorphism](./docs/features/serialization/serialization.md#polymorphism) ([#587](https://github.com/Ambiguous-Interactive/unity-helpers/issues/587), [#601](https://github.com/Ambiguous-Interactive/unity-helpers/issues/601)). +- Add `[WProtoSubtype(typeof(Base))]`, so a subtype joins a WallstopProto hierarchy without picking a field number. The editor assigns and commits the number on the next reload, and **Assign WallstopProto Subtype Tags** retires a removed one -- hand-numbered or not -- so it is never reused. See [Polymorphism](./docs/features/serialization/serialization.md#polymorphism) ([#587](https://github.com/Ambiguous-Interactive/unity-helpers/issues/587), [#601](https://github.com/Ambiguous-Interactive/unity-helpers/issues/601), [#606](https://github.com/Ambiguous-Interactive/unity-helpers/issues/606)). - Add `Sfc64Random`, the Small Fast Chaotic generator: a published-pedigree 64-bit generator with a very small hot path that answers `NextUlong` in one state advance. See [Random Generators](./docs/features/utilities/random-generators.md) ([#516](https://github.com/Ambiguous-Interactive/unity-helpers/issues/516)). - Add a proto schema exporter: **Tools > Wallstop Studios > Unity Helpers > Proto Schema Exporter** writes `proto3` for your `[WProtoContract]` types, so anything downstream can read your saves. Search and tick the exact types, name a package, and write one file or one per assembly, namespace or type ([#424](https://github.com/Ambiguous-Interactive/unity-helpers/issues/424), [#595](https://github.com/Ambiguous-Interactive/unity-helpers/issues/595)). - Add strict UTF-8 validation to WallstopProto strings and Uri: wire bytes that are not valid UTF-8 refuse the payload as malformed instead of decoding to replacement characters, as proto3 requires ([#580](https://github.com/Ambiguous-Interactive/unity-helpers/issues/580)). diff --git a/Editor/Tools/WProtoSubtypeTagPlan.cs b/Editor/Tools/WProtoSubtypeTagPlan.cs index bf29f7043..049550c0f 100644 --- a/Editor/Tools/WProtoSubtypeTagPlan.cs +++ b/Editor/Tools/WProtoSubtypeTagPlan.cs @@ -129,6 +129,7 @@ WProtoSubtypeTagDiscovery discovery ) { List tagless = new List(); + List pinned = new List(); Dictionary> taken = new Dictionary>( StringComparer.Ordinal ); @@ -146,9 +147,14 @@ WProtoSubtypeTagDiscovery discovery if (declaration.HasTag) { + string pinnedKey = PairKey(declaration.SubTypeName, declaration.BaseTypeName); Claim(taken, declaration.BaseTypeName, declaration.Tag); - explicitTags[PairKey(declaration.SubTypeName, declaration.BaseTypeName)] = - declaration.Tag; + if (!explicitTags.ContainsKey(pinnedKey)) + { + explicitTags[pinnedKey] = declaration.Tag; + pinned.Add(declaration); + } + continue; } @@ -169,6 +175,9 @@ WProtoSubtypeTagDiscovery discovery Dictionary retiredByPair = new Dictionary( StringComparer.Ordinal ); + Dictionary allRetired = new Dictionary( + StringComparer.Ordinal + ); foreach (Entry entry in Safe(retired)) { if (!entry.IsUsable) @@ -182,6 +191,12 @@ WProtoSubtypeTagDiscovery discovery { retiredByPair[key] = entry; } + + // Keyed by pair AND number. retiredByPair keeps one entry per pair, which is all a + // restore needs and is NOT enough to re-emit: a pair that retired two numbers -- + // what a hand-edited number leaves behind -- lost one of them on the next run, and + // a dropped retirement is a number that is free again a run later. + allRetired[RetirementKey(entry)] = entry; } List assignments = new List(); @@ -216,15 +231,27 @@ WProtoSubtypeTagDiscovery discovery continue; } - // The subtype pinned its own number and pinned the same one, so the manifest entry - // is simply redundant. Retiring it would forbid the very declaration that now holds - // it, and the next build would refuse a hierarchy that changed in no way at all. - if ( - explicitTags.TryGetValue(key, out int pinned) - && pinned == entry.Tag - && !retiredByPair.ContainsKey(key) - ) + // A number written by hand is as durable a wire contract as one this tool + // assigned, and until it was recorded here the only trace that the number had ever + // been spent was the declaration itself -- which is deleted along with the type it + // sits on. Keeping the entry is what turns that deletion into a retirement (#606). + if (explicitTags.TryGetValue(key, out int pinnedTag)) { + Claim(taken, entry.BaseTypeName, pinnedTag); + assignments.Add( + pinnedTag == entry.Tag + ? entry + : new Entry(entry.SubTypeName, entry.BaseTypeName, pinnedTag) + ); + + // Editing a shipped number in place is the one thing the guidance forbids, and + // it used to leave no trace at all. The number it left still means this type to + // every payload written under it. + if (pinnedTag != entry.Tag) + { + retirements[RetirementKey(entry)] = entry; + } + continue; } @@ -245,6 +272,33 @@ WProtoSubtypeTagDiscovery discovery retirements[RetirementKey(entry)] = entry; } + // Before the tag-less passes, because an explicit number is stated by the source and + // needs neither restoring nor inventing -- it only needs recording. + pinned.Sort(CompareDeclarations); + foreach (Declaration declaration in pinned) + { + string key = PairKey(declaration.SubTypeName, declaration.BaseTypeName); + if (!keptPairs.Add(key)) + { + continue; + } + + assignments.Add( + new Entry(declaration.SubTypeName, declaration.BaseTypeName, declaration.Tag) + ); + + // Remove-then-re-add for the explicit form: the type is back under the number it + // held, so the retirement that was standing in for it is lifted rather than left to + // forbid the very declaration now holding it. + if ( + retiredByPair.TryGetValue(key, out Entry wasRetired) + && wasRetired.Tag == declaration.Tag + ) + { + restoredPairs.Add(key); + } + } + foreach (Entry entry in retiredByPair.Values) { string key = PairKey(entry.SubTypeName, entry.BaseTypeName); @@ -284,7 +338,7 @@ WProtoSubtypeTagDiscovery discovery fresh.Add(assignment); } - foreach (Entry entry in retiredByPair.Values) + foreach (Entry entry in allRetired.Values) { if (!restoredPairs.Contains(PairKey(entry.SubTypeName, entry.BaseTypeName))) { @@ -328,15 +382,21 @@ public string Render(string assemblyName) "// Subtype Tags. Commit it: these numbers are the wire contract for every\r\n" ); builder.Append( - "// [WProtoSubtype] declared without one, so a payload saved today is read back by\r\n" + "// [WProtoSubtype] in this assembly, so a payload saved today is read back by\r\n" + ); + builder.Append( + "// this file. A subtype that wrote its own number is recorded here too, because\r\n" + ); + builder.Append( + "// deleting the type deletes the only other record that the number was spent.\r\n" ); builder.Append( - "// this file. Do not renumber an entry, and do not delete a retired one -- a\r\n" + "// Do not renumber an entry, and do not delete a retired one -- a retired number\r\n" ); builder.Append( - "// retired number is held so a later subtype cannot be given a number old saves\r\n" + "// is held so a later subtype cannot be given a number old saves already mean\r\n" ); - builder.Append("// already mean something else by.\r\n"); + builder.Append("// something else by.\r\n"); builder.Append( "//\r\n// The editor rewrites this file automatically after an assembly reload that finds a\r\n" ); diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs index 19d843f91..838518768 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs @@ -471,7 +471,11 @@ public void AFreshNumberAvoidsTheBasesOwnMembersAndItsIncludes() NoEntries ); - CollectionAssert.AreEqual(new[] { "N.Sub=4" }, Describe(plan.Assigned)); + CollectionAssert.AreEqual( + new[] { "N.Pinned=3", "N.Sub=4" }, + Describe(plan.Assigned), + "N.Sub avoids 1, 2 and 3; N.Pinned is recorded at the number it wrote itself" + ); } [Test] @@ -579,7 +583,9 @@ public void TheRenderedManifestIsWhatTheGeneratorReadsBack() public void APromotedSubtypeKeepsItsNumberWithoutRetiringIt() { // Moving a number out of the manifest and into the attribute changes nothing on the - // wire, so retiring it would forbid the very declaration now holding it. + // wire, so retiring it would forbid the very declaration now holding it. The entry + // stays as the record of a number that has been spent (#606) rather than being dropped + // as redundant -- dropping it was what let the next deletion free the number silently. WProtoSubtypeTagPlan plan = WProtoSubtypeTagPlan.Create( new[] { Declare("N.Sub", "N.Base", 4) }, NoEntries, @@ -587,8 +593,241 @@ public void APromotedSubtypeKeepsItsNumberWithoutRetiringIt() NoEntries ); - Assert.IsEmpty(plan.Assigned); + CollectionAssert.AreEqual(new[] { "N.Sub=4" }, Describe(plan.Assigned)); Assert.IsEmpty(plan.Retired); + Assert.IsEmpty(plan.FreshlyAssigned); + } + + [Test] + public void AnExplicitlyNumberedSubtypeIsRecordedSoItsNumberCanBeRetired() + { + // #606. A number written by hand is as durable a wire contract as one the tool + // assigned, and until this recorded it the only trace that 1 had ever been spent was + // the declaration itself -- which is deleted along with the type it sits on. + WProtoSubtypeTagPlan plan = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Melee", "N.Base", 1) }, + NoEntries, + NoEntries, + NoEntries + ); + + CollectionAssert.AreEqual(new[] { "N.Melee=1" }, Describe(plan.Assigned)); + Assert.IsEmpty( + plan.FreshlyAssigned, + "the number was written by the developer, so nothing was invented and the " + + "automatic pass has no reason to run" + ); + } + + [Test] + public void DeletingAnExplicitlyNumberedSubtypeRetiresItsNumber() + { + // The half #606 is named for: WPROTO039 has no memory, so a number freed by a deletion + // is indistinguishable from one never used unless the deletion leaves a record. + WProtoSubtypeTagPlan recorded = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Melee", "N.Base", 1) }, + NoEntries, + NoEntries, + NoEntries + ); + + WProtoSubtypeTagPlan afterDeletion = WProtoSubtypeTagPlan.Create( + new WProtoSubtypeTagPlan.Declaration[0], + NoEntries, + recorded.Assigned, + recorded.Retired + ); + + CollectionAssert.AreEqual(new[] { "N.Melee=1" }, Describe(afterDeletion.Retired)); + Assert.IsEmpty(afterDeletion.Assigned); + } + + [Test] + public void ANumberFreedByDeletingAnExplicitlyNumberedSubtypeIsNeverHandedOut() + { + // The consequence, end to end. Without the record the next subtype is handed 1 -- the + // smallest free number -- and every payload written by an older build reads that field + // back as the wrong type, with no diagnostic anywhere. + WProtoSubtypeTagPlan recorded = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Melee", "N.Base", 1) }, + NoEntries, + NoEntries, + NoEntries + ); + WProtoSubtypeTagPlan afterDeletion = WProtoSubtypeTagPlan.Create( + new WProtoSubtypeTagPlan.Declaration[0], + NoEntries, + recorded.Assigned, + recorded.Retired + ); + + WProtoSubtypeTagPlan withSuccessor = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Later", "N.Base") }, + NoEntries, + afterDeletion.Assigned, + afterDeletion.Retired + ); + + CollectionAssert.AreEqual(new[] { "N.Later=2" }, Describe(withSuccessor.Assigned)); + CollectionAssert.AreEqual(new[] { "N.Melee=1" }, Describe(withSuccessor.Retired)); + } + + [Test] + public void ReAddingAnExplicitlyNumberedSubtypeTakesBackTheNumberItHeld() + { + // Remove-then-re-add has to keep working for the explicit form too: the type comes back + // with the number it always had, and the retirement it left behind is lifted rather + // than left to forbid the very declaration now holding it. + WProtoSubtypeTagPlan recorded = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Melee", "N.Base", 1) }, + NoEntries, + NoEntries, + NoEntries + ); + WProtoSubtypeTagPlan afterDeletion = WProtoSubtypeTagPlan.Create( + new WProtoSubtypeTagPlan.Declaration[0], + NoEntries, + recorded.Assigned, + recorded.Retired + ); + + WProtoSubtypeTagPlan restored = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Melee", "N.Base", 1) }, + NoEntries, + afterDeletion.Assigned, + afterDeletion.Retired + ); + + CollectionAssert.AreEqual(new[] { "N.Melee=1" }, Describe(restored.Assigned)); + Assert.IsEmpty(restored.Retired, "the number is in use again by the type that held it"); + } + + [Test] + public void DemotingASubtypeToTheManifestKeepsTheNumberItWroteByHand() + { + // The other direction of the promotion case above, and the same defect: with nothing + // recording that the attribute said 1, deleting the number from the source made the + // pair look brand new and it was handed the smallest free number instead. + WProtoSubtypeTagPlan recorded = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base", 40) }, + NoEntries, + NoEntries, + NoEntries + ); + + WProtoSubtypeTagPlan demoted = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base") }, + NoEntries, + recorded.Assigned, + recorded.Retired + ); + + CollectionAssert.AreEqual(new[] { "N.Sub=40" }, Describe(demoted.Assigned)); + Assert.IsEmpty(demoted.Retired); + Assert.IsEmpty( + demoted.FreshlyAssigned, + "40 came from the record, so nothing was invented" + ); + } + + [Test] + public void RenumberingAnExplicitDeclarationRetiresTheNumberItLeft() + { + // Editing a shipped number in place is the thing the guidance forbids, and it used to + // be invisible. The new number is recorded and the old one is retired, so a later + // subtype cannot be given the number old payloads still mean this type by. + WProtoSubtypeTagPlan recorded = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base", 5) }, + NoEntries, + NoEntries, + NoEntries + ); + + WProtoSubtypeTagPlan renumbered = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base", 6) }, + NoEntries, + recorded.Assigned, + recorded.Retired + ); + + CollectionAssert.AreEqual(new[] { "N.Sub=6" }, Describe(renumbered.Assigned)); + CollectionAssert.AreEqual(new[] { "N.Sub=5" }, Describe(renumbered.Retired)); + } + + [Test] + public void AnExplicitDeclarationIsNotRetiredByAnUnattendedPassThatCannotSeeIt() + { + // Recording the explicit form must not weaken the Partial guard: a subtype behind + // #if !UNITY_EDITOR is absent from TypeCache and present in the player, so an + // unattended pass keeps its number claimed rather than retiring it. + WProtoSubtypeTagPlan recorded = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Hidden", "N.Base", 1) }, + NoEntries, + NoEntries, + NoEntries + ); + + WProtoSubtypeTagPlan unattended = WProtoSubtypeTagPlan.Create( + new WProtoSubtypeTagPlan.Declaration[0], + NoEntries, + recorded.Assigned, + recorded.Retired, + WProtoSubtypeTagDiscovery.Partial + ); + + CollectionAssert.AreEqual(new[] { "N.Hidden=1" }, Describe(unattended.Assigned)); + Assert.IsEmpty(unattended.Retired); + } + + [Test] + public void TheGeneratorRefusesAnExplicitSubtypeClaimingARetiredNumber() + { + // The enforcement half. The record is only worth what refuses to spend it again, and + // WPROTO039 cannot: it fires on two LIVE claims, and a retired number has none. + Diagnostic match = Run( + Fixture( + "[assembly: WProtoRetiredSubtypeTag(\"Consumer.Deleted\", typeof(Consumer.Base), 7)]", + "[WProtoSubtype(typeof(Base), 7)]" + ) + ) + .Single(diagnostic => diagnostic.Id == "WPROTO040"); + + StringAssert.Contains("Consumer.Deleted", match.GetMessage()); + StringAssert.Contains("retired", match.GetMessage()); + } + + [Test] + public void TheGeneratorRefusesAnIncludeClaimingARetiredNumber() + { + // [WProtoInclude] on the base and [WProtoSubtype] on the subtype are the same + // declaration written two ways and share one field-number space, so a rule that + // covered only one of them is a rule an author steps around by accident. + Diagnostic match = Run( + "[assembly: WProtoRetiredSubtypeTag(\"Consumer.Deleted\", typeof(Consumer.Base), 7)]" + + "\n[WProtoContract] [WProtoInclude(7, typeof(Sub))] public partial class Base { [WProtoMember(1)] public int A; }" + + "\n[WProtoContract] public partial class Sub : Base { [WProtoMember(1)] public int B; }" + ) + .Single(diagnostic => diagnostic.Id == "WPROTO013"); + + StringAssert.Contains("Consumer.Deleted", match.GetMessage()); + StringAssert.Contains("retired", match.GetMessage()); + } + + [Test] + public void TheGeneratorLetsARetiredTypeReclaimItsOwnNumber() + { + // Re-adding the type the number belonged to is the case retirement exists to serve, so + // the refusal is about the NAME, not about the number alone. + Assert.IsEmpty( + Describe( + Run( + Fixture( + "[assembly: WProtoRetiredSubtypeTag(\"Consumer.Sub\", typeof(Consumer.Base), 7)]", + "[WProtoSubtype(typeof(Base), 7)]" + ) + ) + ) + ); } [Test] @@ -1124,8 +1363,15 @@ public void ASecondPassOverAnAlreadyWrittenManifestWritesNothing() } [Test] - public void AnAssemblyWhoseSubtypesAllWriteTheirOwnNumbersGetsNoManifestAtAll() - { + public void AdoptingThePackageWritesNoManifestIntoAProjectThatInventsNoNumbers() + { + // The guard that "adopting the package must not put a file into a project" rests on, + // now that a hand-written number is recorded rather than dropped (#606). It was + // plan.IsEmpty, which said the same thing only for as long as such a plan had nothing + // in it; the real gate is the one the unattended pass reads -- + // WProtoSubtypeTagAssigner skips the write when FreshlyAssigned is empty, so a project + // that numbers its own subtypes gets a file only from a deliberate menu run whose diff + // a human reads. WProtoSubtypeTagPlan plan = WProtoSubtypeTagPlan.Create( new[] { Declare("N.Sub", "N.Base", 4) }, NoEntries, @@ -1133,10 +1379,14 @@ public void AnAssemblyWhoseSubtypesAllWriteTheirOwnNumbersGetsNoManifestAtAll() NoEntries ); - Assert.IsTrue(plan.IsEmpty); - Assert.IsFalse( - WProtoSubtypeTagManifestFile.NeedsWrite(null, plan.Render("A"), plan.IsEmpty), - "adopting the package must not put a file into a project that never uses the form" + Assert.IsEmpty( + plan.FreshlyAssigned, + "nothing was invented, so the automatic pass has no reason to write" + ); + CollectionAssert.AreEqual( + new[] { "N.Sub=4" }, + Describe(plan.Assigned), + "and an explicit run records the number, which is what makes a later deletion visible" ); } diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs index 1522eaf7f..297624c4e 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs @@ -422,11 +422,48 @@ out string problem "field number " + tag + " is outside 1-536870911 or inside the reserved 19000-19999 range"; + return true; + } + + // A hand-written number is checked against the retirement record, and a manifest one is + // not: an entry that collides with a retirement is WPROTO042 at the manifest line that + // holds it, and reporting the same collision twice sends the developer to the + // declaration rather than to the file the number actually lives in. The name is what + // decides, not the number -- re-adding the type the number belonged to is the case + // retirement exists to serve (#606). + if ( + !tagless + && manifest.TryRetired(baseType, tag, out string retiredBy) + && retiredBy != subType.ToDisplayString() + ) + { + problem = RetiredProblem(tag, baseType, retiredBy); } return true; } + /// + /// Explains why a retired field number cannot be handed to another subtype. + /// + /// The field number being claimed. + /// The base it lives on. + /// The fully qualified name of the type that held it. + /// The clause a subtype or include diagnostic appends. + internal static string RetiredProblem(int tag, INamedTypeSymbol baseType, string retiredBy) + { + return "field number " + + tag + + " on '" + + (baseType == null ? "?" : baseType.Name) + + "' is retired, having belonged to '" + + retiredBy + + "'. Payloads written before that type was removed still carry it under this " + + "number, so handing it to another type reads those saves back as the wrong " + + "type. Give this one a free number, or restore the deleted type under its own " + + "name"; + } + /// /// Reports whether or anything enclosing it takes type arguments. /// diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeTagManifest.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeTagManifest.cs index d0333ad74..5e948f4fb 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeTagManifest.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeTagManifest.cs @@ -44,14 +44,20 @@ internal sealed class SubtypeTagManifest "WallstopStudios.UnityHelpers.Core.Serialization.WallstopProto.WProtoRetiredSubtypeTagAttribute"; private static readonly SubtypeTagManifest EmptyManifest = new SubtypeTagManifest( - new Dictionary(StringComparer.Ordinal) + new Dictionary(StringComparer.Ordinal), + new Dictionary(StringComparer.Ordinal) ); private readonly Dictionary _assigned; + private readonly Dictionary _retired; - private SubtypeTagManifest(Dictionary assigned) + private SubtypeTagManifest( + Dictionary assigned, + Dictionary retired + ) { _assigned = assigned; + _retired = retired; } /// A manifest with no entries, for a compilation that declares none. @@ -70,12 +76,35 @@ private SubtypeTagManifest(Dictionary assigned) internal static SubtypeTagManifest Build(Compilation compilation) { Dictionary assigned = new Dictionary(StringComparer.Ordinal); + Dictionary retired = new Dictionary( + StringComparer.Ordinal + ); foreach (AttributeData attribute in compilation.Assembly.GetAttributes()) { + if ( + TryReadEntry( + attribute, + RetiredAttribute, + out string retiredName, + out INamedTypeSymbol retiredBase, + out int retiredTag + ) + ) + { + string retiredKey = TagKeyOf(retiredBase, retiredTag); + if (!retired.ContainsKey(retiredKey)) + { + retired[retiredKey] = retiredName; + } + + continue; + } + if ( !TryReadEntry( attribute, + TagAttribute, out string subTypeName, out INamedTypeSymbol baseType, out int tag @@ -92,7 +121,34 @@ out int tag } } - return assigned.Count == 0 ? EmptyManifest : new SubtypeTagManifest(assigned); + return assigned.Count == 0 && retired.Count == 0 + ? EmptyManifest + : new SubtypeTagManifest(assigned, retired); + } + + /// + /// Looks up the subtype a retired field number used to belong to. + /// + /// The base the number lives on. + /// The field number being claimed. + /// The fully qualified name of the type that held it. + /// false when the number is not retired on that base. + /// + /// The enforcement half of the retirement record. WPROTO039 fires when two types + /// claim one number at the same TIME and so has no memory: a number freed by a deletion is + /// indistinguishable from one never used, and handing it to a later subtype reads every + /// payload written by an older build back as the wrong type + /// (#606). + /// + internal bool TryRetired(INamedTypeSymbol baseType, int tag, out string retiredBy) + { + if (baseType == null) + { + retiredBy = null; + return false; + } + + return _retired.TryGetValue(TagKeyOf(baseType, tag), out retiredBy); } /// @@ -289,6 +345,7 @@ internal static void Validate(Compilation compilation, Action report private static bool TryReadEntry( AttributeData attribute, + string attributeName, out string subTypeName, out INamedTypeSymbol baseType, out int tag @@ -296,7 +353,7 @@ out int tag { if ( attribute.AttributeClass == null - || attribute.AttributeClass.ToDisplayString() != TagAttribute + || attribute.AttributeClass.ToDisplayString() != attributeName || attribute.ConstructorArguments.Length < 3 ) { diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs index db74592b9..d79b55c42 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs @@ -2956,6 +2956,16 @@ SubtypeMap subtypes + tag + " is outside 1-536870911 or inside the reserved 19000-19999 range"; } + else if ( + subtypes.Manifest.TryRetired(contract, tag, out string retiredBy) + && retiredBy != subType.ToDisplayString() + ) + { + // The two declaration forms share one field-number space, so a rule that + // covered only [WProtoSubtype] would be one an author steps around by + // accident (#606). + problem = SubtypeMap.RetiredProblem(tag, contract, retiredBy); + } else if (!claimed.Add(tag)) { problem = diff --git a/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll b/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll index 9965697bad2cb8a6141c9b4742495549b96eb87d..0542597540e52b0b5650114bd118b0a4e2951cd3 100644 GIT binary patch delta 43165 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 EU!PEQ%>V!Z diff --git a/Tests/Runtime/WProtoSubtypeTags.cs b/Tests/Runtime/WProtoSubtypeTags.cs index a6176e220..bee52bdd0 100644 --- a/Tests/Runtime/WProtoSubtypeTags.cs +++ b/Tests/Runtime/WProtoSubtypeTags.cs @@ -4,10 +4,12 @@ // WallstopProto subtype tag manifest for WallstopStudios.UnityHelpers.Tests.Runtime. // Written by Tools > Wallstop Studios > Unity Helpers > Assign WallstopProto // Subtype Tags. Commit it: these numbers are the wire contract for every -// [WProtoSubtype] declared without one, so a payload saved today is read back by -// this file. Do not renumber an entry, and do not delete a retired one -- a -// retired number is held so a later subtype cannot be given a number old saves -// already mean something else by. +// [WProtoSubtype] in this assembly, so a payload saved today is read back by +// this file. A subtype that wrote its own number is recorded here too, because +// deleting the type deletes the only other record that the number was spent. +// Do not renumber an entry, and do not delete a retired one -- a retired number +// is held so a later subtype cannot be given a number old saves already mean +// something else by. // // The editor rewrites this file automatically after an assembly reload that finds a // [WProtoSubtype] with no number and no entry here, so adding a subtype is one diff --git a/docs/features/serialization/serialization.md b/docs/features/serialization/serialization.md index 47add8ad6..c4f0d26ac 100644 --- a/docs/features/serialization/serialization.md +++ b/docs/features/serialization/serialization.md @@ -1552,6 +1552,12 @@ the tool enforces all three: take that number, so a payload saved before the deletion cannot come back as some later type. - **Re-adding the type restores its own number.** The retired entry is matched by name and turned back into an assignment. +- **A number you wrote by hand is recorded too.** `[WProtoSubtype(typeof(Weapon), 3)]` gets an + entry beside the assigned ones, because the declaration is otherwise the only record that 3 + was ever spent -- and it is deleted along with the type that carries it. With the entry, the + deletion is seen and 3 is retired; without it, the next subtype added is handed 3 and every + payload written by an older build reads that field back as the wrong type. `[WProtoInclude]` + is the same declaration written on the base and is covered the same way. **The subtype half of an entry is a string, not a `typeof`, and that is what makes retirement possible.** A `typeof` stops compiling the moment the subtype is deleted, and the only cheap repair From c68433123501c5c86fd230de8fbf6cf50771f5de Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 00:04:13 +0000 Subject: [PATCH 03/15] Refuse a cross-assembly subtype permanently, and say what to write #603 asked for a decision: build a runtime registry so a consumer can derive from a package base, or keep refusing and explain why the refusal is not temporary. PLAN.md names refusal the safe default, and it is: every failure mode of a registry is silent data corruption rather than a build error. Unity's registrars run unordered, so a serialize before the last registrar writes under the wrong number or none; two packages picking one number on a shared base is undetectable at build time and type-confusing at read time; and the lookup has to survive managed stripping under IL2CPP. A build error you can see beats a player that writes an unreadable save. WPROTO040's message said the base's chain "was generated when its own assembly was compiled" and left the reader to guess whether that was a limit or a gap. It now states that this is how per-assembly generation works rather than a gap waiting to be filled, and names composition -- your own [WProtoContract] holding the base as a [WProtoMember] -- as the shape that does work. A diagnostic that names a fix has to name one that works, so a second test compiles that alternative against an upstream assembly and asserts it generates clean. The docs carry both shapes side by side, and say plainly what composition costs: a List cannot hold your type. Fixes #603 Co-Authored-By: Claude Opus 5 (1M context) --- .../DiagnosticTests.cs | 64 ++++++++++++++++++ .../SubtypeMap.cs | 24 +++++-- ...opStudios.UnityHelpers.Proto.Generator.dll | Bin 194560 -> 195584 bytes docs/features/serialization/serialization.md | 52 +++++++++++--- 4 files changed, 127 insertions(+), 13 deletions(-) diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs index 55537e598..862fdb74b 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs @@ -513,6 +513,70 @@ public void ASubtypeCannotDeclareItselfAgainstABaseInAnotherAssembly() Assert.IsTrue(match.GetMessage().Contains("ConsumerAssembly"), match.GetMessage()); } + ///

+ /// The refusal states a permanent reason and a fix, rather than implying a future release. + /// + /// + /// The decision recorded on + /// #603. + /// The alternative is a runtime registry whose every failure mode -- Unity's registrars + /// running unordered, two packages claiming one tag on a shared base, a lookup stripped + /// under IL2CPP -- is silent data corruption rather than a build error. A developer reading + /// this message needs to know it will not change, and what to write instead. + /// + [Test] + public void TheCrossAssemblyRefusalNamesAPermanentReasonAndAWorkingAlternative() + { + MetadataReference upstream = CompileReference( + "UpstreamAssembly", + @"namespace Upstream { using WallstopStudios.UnityHelpers.Core.Serialization.WallstopProto; + [WProtoContract] public partial class Base { [WProtoMember(1)] public int A; } }" + ); + + string message = Run( + @"[WProtoContract] [WProtoSubtype(typeof(Upstream.Base), 100)] public partial class Sub : Upstream.Base { [WProtoMember(1)] public int B; }", + upstream + ) + .Single(diagnostic => diagnostic.Id == "WPROTO040") + .GetMessage(); + + StringAssert.Contains("rather than a gap waiting to be filled", message); + StringAssert.Contains("[WProtoMember]", message); + foreach (string implication in new[] { "not yet", "for now", "until", "in a future" }) + { + StringAssert.DoesNotContain(implication, message); + } + } + + /// + /// The alternative the refusal recommends compiles and generates, in the consumer assembly. + /// + /// + /// A diagnostic that names a fix has to name one that works, or the developer spends the + /// refusal twice. Composition is what a per-assembly generator CAN honour: the member's + /// declared type resolves through the upstream assembly's own formatter, which carries the + /// upstream subtypes in the chain that was emitted with it. + /// + [Test] + public void TheAlternativeTheCrossAssemblyRefusalRecommendsGenerates() + { + MetadataReference upstream = CompileReference( + "UpstreamAssembly", + @"namespace Upstream { using WallstopStudios.UnityHelpers.Core.Serialization.WallstopProto; + [WProtoContract] [WProtoInclude(100, typeof(UpstreamSub))] public partial class Base { [WProtoMember(1)] public int A; } + [WProtoContract] public partial class UpstreamSub : Base { [WProtoMember(1)] public int C; } }" + ); + + CollectionAssert.IsEmpty( + Run( + @"[WProtoContract] public partial class Holder { [WProtoMember(1)] public Upstream.Base Wrapped; [WProtoMember(2)] public int B; }", + upstream + ) + .Select(diagnostic => diagnostic.Id + " " + diagnostic.GetMessage()) + .ToArray() + ); + } + /// /// The two declaration forms emit the same formatter, character for character. /// diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs index 297624c4e..1947a96db 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs @@ -383,6 +383,14 @@ out string problem ) ) { + // Permanent, and the message says so. A per-assembly generator emits the base's + // dispatch chain when the base's assembly compiles; a subtype declared afterwards + // in a referencing assembly is not late to a list, it is outside the compilation + // that built the list. Closing the gap needs a runtime registry whose every + // failure mode -- unordered registrars, two packages claiming one tag, a lookup + // stripping under IL2CPP -- is silent data corruption rather than a build error, + // which is a worse trade than this refusal + // (https://github.com/Ambiguous-Interactive/unity-helpers/issues/603). problem = "'" + baseType.Name @@ -392,13 +400,21 @@ out string problem + subType.Name + "' into '" + (subType.ContainingAssembly == null ? "?" : subType.ContainingAssembly.Name) - + "'. The base's dispatch chain was generated when its own assembly was compiled " - + "and nothing added later can appear in it, so this subtype would compile and " - + "then throw on the first save. Move '" + + "'. The base's dispatch chain is generated when its own assembly is compiled, " + + "so a subtype declared afterwards in an assembly that references it can never " + + "appear in that chain. This is how per-assembly generation works rather than a " + + "gap waiting to be filled, and accepting the declaration would compile and " + + "then throw on the first save. Either move '" + subType.Name + "' into '" + (baseType.ContainingAssembly == null ? "?" : baseType.ContainingAssembly.Name) - + "', or hold it behind a contract of its own rather than as its base"; + + "', or give '" + + subType.Name + + "' a [WProtoContract] of its own and hold a '" + + baseType.Name + + "' in it as a [WProtoMember] instead of deriving from it -- a member of a " + + "type from another assembly is generated normally, and the base writes its " + + "own subtypes through its own chain"; return true; } diff --git a/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll b/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll index 0542597540e52b0b5650114bd118b0a4e2951cd3..a95dc58f4cebd952a8bd7281c5c67fc4e60ec114 100644 GIT binary patch delta 8198 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 Pxm+!_bI`^cT;cx*T2y2m delta 7717 zcma)>34Dy#`p3_CCYemONhUK{CJ7?4XC{)Fsm5{@RaHy5rL|sCRB17DU$m-Zq_GCA zI5deRF6vrKbP!ZYwANZeX)U3)YPt!ns-pUTp7(u|sLtpAx$^PM`JUhJIp=xLyPSEG zDR%>>+zgyIGyZIDaS>DhU1ITZfjpD38^D-O=^PVWxb7u=Y_Nv)ppG~Vo9a{8)ELxG z!-A*-g;>!PETSJz4jA_nCdOg!4N<_;`AxBnC-XbvB9AxsL?iv{jQ@f8vM3{GHNSJl zi`xGTPkMU7glW8~=g^EVx$Y2J^nOi(#p+qxA#t#nRnR$j!(is}O_{-1fOK%MxLDAu zHTA4Wj0|CN?~24IscR1Pm^Bui#`z37(|sjNKkAB>`p8m@&x*uYX=#pU)NBifQA5O{ zId(`N>N!3~Bjc&L#W~eOTOhm$rxtP zE29QUMXv~HULRBGaHYr)xj;JXFNcc@^IF)|r3$MXs(z^q4c6LMDLKYqq$NrmAvzV> z|ED~4Weq0EJ2%3!p|CA~f+kc?44y9LO3A^)PkSDukk^ zYaZKzWchQz!Yi7Hy_g~K80K=8VUl!^@YrjfUL^qTlLXf(I9|uleKd2KBTc3hAI8$h z-bVYBMU44KBe7qaOyM{MFSf;Gs~CggDGCm!ciR%Og};SjDh0QjqL<;8l5faF6yKn5 zoq`uzDcJ^0LeWX#00l3$Wn`=Ty(Ud zkgLu`D9H>k(`Zrf`UY zm-X6C$t{kf*oDGj3V5|O`A>w~dK%m9DQu-c$&Ve#PD&ne4#i0nZd34L`Z|HQM^ZCc#U`4ZnFLGJ&J!)c=ZSGwmoF4zJcOR z3O#GrcX@=t_jhcYDdbb=KH9Tt`F}O= z{5?!0pU0B}ed8CXuk(0sHTX{;RJ8yDvJd=>gIAJz`BK@PFez zbK$ODdS6^wxAzhHaQ%qT-KeFEq4fvCJDmTXZV4dRYk1YB)#$C=l9NQ2qbz{QpX>Xi_T^i@VaFbCt zu=z}k_Sm49iRB&(%x9kC9v6q*_dV^m26I@)M9*z@M>P$ECoe$b=S<)%KyFJ9>Uxv> zBKrpVX`CS_*~&pOaqiNJ5qoUnyKSQ&fr~!n@%FaZDHj8=bB8m=6CfOCav= zY)UK9_~ULGhe=mAi?kY>KGFt%Z(}}EUi&~%x;vVdaOdufp4S2~pO(-+Wim$jyM(%o zb&53JAHOeaUC)rJaBw#hZ&hWa zK622#6Y7Rmr!B4TkY*%`s;X!>6@?#oM;{FnIY$kiloNwB_(T-5P8Gn3L=k?vBZMc3 z0jK||r+1|=fB7YkUTt@t?qRBXUsHPN7JD;EZ2HRVY4PPi-bhsq24_pW0gc8mqxyyz zC9O^JZ2HQ;4ZkFD+9$UMD4U;OvN&<(1>BQF;#p_rBcc?OOgBsqd|Wo%2W5 z3f^7K#J6X=ManzuIJ~xuT7$M_e|*(9_|it(<=k4XUzCD3(Ni__ z@jZgFQJelWX4G%H*nL~+wqSSt28`X&sj@p9yKyU4w{<_MQ>y2M3(*{0sbb8~8+??V+vtna zM5NUV!M^_g)Z@3#H1Y7V+5b-zP&cQGxGR(6iEtmtJ&oUZ=)APE$dB~gFJ+FFMp?w3 zD=E;!D!#i?0dHBw`m4<$B283ZO%0*BojEjhW1WN!)3rCDCe<_dTD1nISv{}URB~vQ z;mP>^B8TrXJo+DVAeNRbWhg$Ptg#w-u@s2c;1Y>~8>e7NbCFZq9)@O$m9;HlX{NYX z>wuM+9@9;iZ}gT-&YHd0i%qTMEEFHC&7OVnxsLd`pwd(N>&#g9ZG2T?Ot7>jp3Jv- z&$UE_Y@X3+&E*qRnrQqu)6L6NI&c0oGmmq8OG0N<^R%Ox`8-&ryr|T)6G+Wu61q)K zUCo@vEef$u0&0*R$du2DeQPtPbF&-Yq8Q6(oy~VMXYqD233lD|k#!EQ#+Mi>#`+Xw zy~|7S-Jj@Uf+0)r3Y9JdFKE7ipHb#QaGpqltEdxxy0{Kt>Tsg$;i_r{kJ8uX6MXrycF;@TY zto8hoN$$87-utjZ zpINV3kMQcx6^cuav>oT|jwsYAqmiwOUsFl%NV1*eRVNkgjm$Q-GrZyph4zI#Ydgz- zCUWD1BD06>9QQe;i2cn2Z0GrBDm6|TVY|Qw;=M(s{UiKcTMZxel|r+F$7lS&gU%?_ zBF$<2iFZ`#i}Xge8+@utD>9ebZt}Bd73amaPU|oHwoL9q)*-=Zz0GsZDJ>K_gg}SDFZF|VeR7y{;u`w8T zS<#|RcWoLNcts{R>k@v~<_~SID<zd!ETQR*AEIg6585hVN|U`gIh=1 z;^D4J@wO+d&0xY!MO)N-ussPT-BPHld7?cH_NqkJlLd~b)?Q%YiZ(dG?X>oW4^(v8dt|%_nV^WN z*2a#Ppq)x9jFFB$P%YDRd|qf-U$~}d^24Mr#A_%opG|O(W>d*6Q&*MVh$U^HN>f8a z-H5B}G2PJ*4%Jgm^8s+Hp0XT+AkIgbVrpnh#}Ig_o;o;Qg$4E0%`qCjmno0shK90n zkl;&sd2B#aEgKJQWZKV0#N=`Jcz8}m<(S69K$-IK2>Uq3!&rrMgMB|{6X0!?SSzCZ zIxT#NV*<>s&l`cX_z~wsSgF#0&^$g7*2_fmaW>Hn`((V#=b5zZZKzhMpmjXb2`$xq z*}p8?>3AE?D#Ul#$$3fDl3F^DZmL>~)bZ%F`^kBHV`^8_TFXR-GzGO66w-xhPqJKa z_g8UuBH<8~2IDd1!fP@qmFB`!nUq4^utX&))D4?;N}(;YXFJ@mLm|0PH&mz^73zj6 zRr@5R5S>Q1e?z5ih>=OD)D1R;bl=3DWbZ&Ll^}_zgGyBBJJ3ZYrO8K1t&ZuC9mw5iz0OAlS0L_?Mdd=A0Y_Bp zR7{)!r)0|0EomCcX21ni+k)Cm_(s(}M{OqDP_-+l&4N3s_5ih6@TaOp#OLt>@C)L! z|K;hL$A_{42t}l$xYstG%?A9X0TJVI`yI1khfGRobD>fuWy75dNB^RoR`YJLV}Wy_ zTBXtah+{7Np;87r<#-Qt!8n5(r`W{KIp)DILZlFW&QS;zDjj2&9P{C_N)oGaEP&f8 zS$M5uA!zlKrgKl;hdPz(zclCS4tSgdMvl@!AVs`gXzeMd2Ts}fx$#ZW7ga#`hdmq6#RhUrUSm`deXObJYp>9T+DliDXRQ6agQ5_nJ5sF)HcR<+s5f#^K15*1Sd zS7lO8S_#z3G(Ad3Px2DD8&)3Gh?nk%yvaq|uku#DZ?c!!*rIlAAhIav8n&aumzfKHUcpc59Vz#XM3)6=EOTNU$3|fP z*rLj@U@W@(4bKVfj{R?7P8>^Se9E-sqMSYuZlJ6u~V*<%WAu%L_C8{tr1D{}=!O diff --git a/docs/features/serialization/serialization.md b/docs/features/serialization/serialization.md index c4f0d26ac..70e500d8c 100644 --- a/docs/features/serialization/serialization.md +++ b/docs/features/serialization/serialization.md @@ -1607,15 +1607,49 @@ A `[WProtoSubtype]` must name the annotated type's **immediate** base, which mus `[WProtoContract]` **in the same assembly**, with a field number that is free. Neither type may be generic: one formatter serves every closure of a generic definition, and one field number cannot identify a type that is really as many types as it has closures. Anything else is a build error -(`WPROTO040`) naming the type, the base and what is wrong. The same-assembly rule is -where this feature stops: the base's dispatch chain is generated when the base's own assembly is -compiled, and a declaration made afterwards, in a package that references it, could never appear -there. **The manifest does not change this.** A number is only half the problem: two packages that -never see each other cannot coordinate one, Unity's registrars run unordered so a serialize before -every registrar has run would write under the wrong number or none, and a registry lookup has to -stay IL2CPP-safe. That is a different mechanism with a different failure mode, and it is tracked -separately. Until then, keep a hierarchy inside one assembly, or hold the foreign type behind a -contract of its own rather than as its base. +(`WPROTO040`) naming the type, the base and what is wrong. + +##### Why a hierarchy cannot cross an assembly boundary + +The same-assembly rule is where this feature stops, and it stops there permanently. The base's +dispatch chain is generated when the base's own assembly is compiled, so a subtype declared +afterwards, in an assembly that references it, is not late to a list -- it is outside the compilation +that built the list. **The manifest does not change this**, and neither would a bigger one: two +packages that never see each other cannot agree a number. + +Closing the gap would need a runtime registry, and every one of its failure modes is silent data +corruption rather than a build error. Unity's registrars run unordered, so a serialize that happens +before every registrar has run writes under the wrong number or none. Two unrelated packages picking +the same number on a shared base is undetectable at build time and type-confusing at read time. And +a registry lookup has to stay IL2CPP-safe and survive managed stripping. A build error you can see is +a better trade than a player that writes an unreadable save, so the refusal stands. + +Two shapes work instead. Keep the hierarchy inside one assembly -- or, when the base belongs to +somebody else, **compose rather than derive**: + +```csharp +// Refused: Sub is in your assembly, Weapon is in the package's. +[WProtoContract] +[WProtoSubtype(typeof(Weapon), 100)] +public partial class PlasmaCutter : Weapon { } + +// Supported: your type is its own contract and holds a Weapon. +[WProtoContract] +public partial class PlasmaCutter +{ + [WProtoMember(1)] + public Weapon Base; + + [WProtoMember(2)] + public float ChargeSeconds; +} +``` + +A member whose type comes from another assembly is generated normally, and `Weapon` still writes its +own subtypes through the chain that was emitted with it -- so a `Weapon` field holding a package +subtype round-trips as that subtype. What you give up is being _dispatched as_ a `Weapon`: a +collection declared `List` cannot hold a `PlasmaCutter`. Declare the collection as your own +type instead. #### Surrogates From e865c3763a18f1eed6e7ab9613e1e08d83274c8e Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 00:38:59 +0000 Subject: [PATCH 04/15] Run project validation headlessly, with a reviewable suppression file The validation engine shipped without a way for anything but a person to run it, which is the difference between "you can write project checks" and "your project is checked". This is the headless half. ValidationBatch.ValidateFromCommandLine finds every IValidationRule through TypeCache, builds it, runs the lot, writes a JSON report and exits non-zero when anything at or above -validationFailOn stands unsuppressed. A rule that threw fails the run whatever the threshold: it produced no answer for that asset, and passing on it would report coverage the run does not have. A rule that cannot be constructed is reported and skipped rather than ending the run, because one broken rule hiding every other rule's findings is worse. ValidationSuppressions is one finding identity per line, so a diff shows exactly which check somebody switched off. It matches on rule, asset GUID and discriminator -- never the path, never the message -- so moving an asset or rewording a rule does not silently un-suppress it. Render() writes the path and message above each entry as a comment, because a rule name and a GUID tell a reviewer nothing. UnusedIn reports entries that matched nothing. A suppression that outlives the finding it silenced reads as a considered decision and is really a line nobody has looked at -- the same shape as a linter that cannot report. Its doc says plainly that only a whole-project run may be trusted for it. The report keeps suppressed findings and marks them, rather than dropping them: a report that omitted them would make a suppression file indistinguishable from a project with nothing wrong. Verified: compiles against Unity reference assemblies through typecheck:editor and typecheck:tests, changed-file preflight and the five relevant repo-lint gates pass. The 18 EditMode fixtures run in CI -- no Unity license or MCP bridge was reachable this session, so their execution is unverified locally. The JSON assertions round-trip through JsonUtility rather than matching pretty-printed spacing, so they test content rather than indentation. Addresses #288 (results window and automatic re-runs still open) Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- .../Validation/Continuous/ValidationBatch.cs | 346 ++++++++++++ .../Continuous/ValidationBatch.cs.meta | 11 + .../Validation/Continuous/ValidationReport.cs | 238 +++++++++ .../Continuous/ValidationReport.cs.meta | 11 + .../Continuous/ValidationSuppressions.cs | 207 ++++++++ .../Continuous/ValidationSuppressions.cs.meta | 11 + .../Validation/ValidationReportingTests.cs | 492 ++++++++++++++++++ .../ValidationReportingTests.cs.meta | 11 + .../features/editor-tools/asset-validation.md | 63 ++- 10 files changed, 1386 insertions(+), 6 deletions(-) create mode 100644 Editor/Validation/Continuous/ValidationBatch.cs create mode 100644 Editor/Validation/Continuous/ValidationBatch.cs.meta create mode 100644 Editor/Validation/Continuous/ValidationReport.cs create mode 100644 Editor/Validation/Continuous/ValidationReport.cs.meta create mode 100644 Editor/Validation/Continuous/ValidationSuppressions.cs create mode 100644 Editor/Validation/Continuous/ValidationSuppressions.cs.meta create mode 100644 Tests/Editor/Validation/ValidationReportingTests.cs create mode 100644 Tests/Editor/Validation/ValidationReportingTests.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 6865b663e..b44004580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add an editor validation engine: implement `IValidationRule` and run it across the whole project a few milliseconds per editor tick instead of freezing for thirty seconds. Only the assets a rule claims are loaded. See [Asset Validation](./docs/features/editor-tools/asset-validation.md) ([#288](https://github.com/Ambiguous-Interactive/unity-helpers/issues/288)). +- Add an editor validation engine: implement `IValidationRule` and run it across the whole project a few milliseconds per tick, not one thirty-second freeze. Only claimed assets load, and one `-executeMethod` runs it in CI with a JSON report and a reviewable suppression file. See [Asset Validation](./docs/features/editor-tools/asset-validation.md) ([#288](https://github.com/Ambiguous-Interactive/unity-helpers/issues/288)). - Add `[WProtoSubtype(typeof(Base))]`, so a subtype joins a WallstopProto hierarchy without picking a field number. The editor assigns and commits the number on the next reload, and **Assign WallstopProto Subtype Tags** retires a removed one -- hand-numbered or not -- so it is never reused. See [Polymorphism](./docs/features/serialization/serialization.md#polymorphism) ([#587](https://github.com/Ambiguous-Interactive/unity-helpers/issues/587), [#601](https://github.com/Ambiguous-Interactive/unity-helpers/issues/601), [#606](https://github.com/Ambiguous-Interactive/unity-helpers/issues/606)). - Add `Sfc64Random`, the Small Fast Chaotic generator: a published-pedigree 64-bit generator with a very small hot path that answers `NextUlong` in one state advance. See [Random Generators](./docs/features/utilities/random-generators.md) ([#516](https://github.com/Ambiguous-Interactive/unity-helpers/issues/516)). - Add a proto schema exporter: **Tools > Wallstop Studios > Unity Helpers > Proto Schema Exporter** writes `proto3` for your `[WProtoContract]` types, so anything downstream can read your saves. Search and tick the exact types, name a package, and write one file or one per assembly, namespace or type ([#424](https://github.com/Ambiguous-Interactive/unity-helpers/issues/424), [#595](https://github.com/Ambiguous-Interactive/unity-helpers/issues/595)). diff --git a/Editor/Validation/Continuous/ValidationBatch.cs b/Editor/Validation/Continuous/ValidationBatch.cs new file mode 100644 index 000000000..856adbc21 --- /dev/null +++ b/Editor/Validation/Continuous/ValidationBatch.cs @@ -0,0 +1,346 @@ +// MIT License - Copyright (c) 2026 wallstop +// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE + +namespace WallstopStudios.UnityHelpers.Editor.Validation.Continuous +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using System.IO; + using UnityEditor; + using UnityEngine; + + /// + /// Runs every in the project from the command line and reports + /// what it found, so a build can refuse to publish an asset nobody would have looked at. + /// + /// + /// + /// The whole point of the engine is continuous, lightweight checks. That only becomes a + /// guarantee when something other than a person is running it, which is what this is for: + /// + /// + /// Unity -batchmode -quit -projectPath <project> \ + /// -executeMethod WallstopStudios.UnityHelpers.Editor.Validation.Continuous.ValidationBatch.ValidateFromCommandLine \ + /// -validationOutput validation.json -validationFailOn Warning + /// + /// + /// It exits non-zero when anything at or above the threshold stands unsuppressed, and when any + /// rule threw -- a rule that threw produced no answer for that asset, so passing on it would be + /// reporting coverage the run does not have. + /// + /// + /// Rules are found through TypeCache and constructed with their parameterless + /// constructor. A rule that cannot be constructed is reported and skipped rather than ending + /// the run, because the alternative is one broken rule hiding every other rule's findings. + /// + /// + public static class ValidationBatch + { + /// The argument naming where the JSON report is written. + public const string OutputArgument = "-validationOutput"; + + /// The argument naming the suppression file to read. + public const string SuppressionsArgument = "-validationSuppressions"; + + /// The argument naming the lowest severity that fails the run. + public const string FailOnArgument = "-validationFailOn"; + + /// The argument naming a folder to restrict the run to; repeatable. + public const string FolderArgument = "-validationFolder"; + + /// + /// Validates the project and exits with 0 when nothing blocking stands. + /// + public static void ValidateFromCommandLine() + { + Result result = Run(Environment.GetCommandLineArgs()); + Debug.Log(result.Summary); + EditorApplication.Exit(result.ExitCode); + } + + /// + /// Validates the project according to a command line, without exiting. + /// + /// The process arguments; null is treated as none. + /// What happened, including the exit code the caller should use. + /// + /// Separated from so the decision can be made without + /// killing the editor, which is what lets a menu item and a test reach it. + /// + public static Result Run(string[] commandLine) + { + List folders = ValuesOf(commandLine, FolderArgument); + string outputPath = ValueOf(commandLine, OutputArgument); + string suppressionsPath = ValueOf(commandLine, SuppressionsArgument); + ValidationSeverity threshold = ParseSeverity( + ValueOf(commandLine, FailOnArgument), + ValidationSeverity.Error + ); + + List problems = new List(); + List rules = DiscoverRules(problems); + ValidationSuppressions suppressions = ReadSuppressions(suppressionsPath, problems); + + ValidationRun run = new ValidationRun( + rules, + ValidationTargets.Enumerate(folders.ToArray()) + ); + while (!run.Step(double.MaxValue)) { } + + string json = ValidationReport.ToJson(run, suppressions); + if ( + !string.IsNullOrEmpty(outputPath) && !TryWrite(outputPath, json, out string failure) + ) + { + problems.Add(failure); + } + + bool blocking = ValidationReport.HasBlockingResults(run, suppressions, threshold); + return new Result(run, suppressions, json, problems, blocking || 0 < problems.Count); + } + + /// + /// Constructs one instance of every concrete rule the project defines. + /// + /// Receives one line per rule that could not be constructed. + /// The rules, ordered by type name so two runs agree. + public static List DiscoverRules(List problems) + { + List candidates = new List(); + foreach (Type candidate in TypeCache.GetTypesDerivedFrom()) + { + if ( + candidate == null + || candidate.IsAbstract + || candidate.IsInterface + || candidate.ContainsGenericParameters + ) + { + continue; + } + + candidates.Add(candidate); + } + + // TypeCache's order is not a property of the project, and a report whose findings + // arrive in a different order on two machines cannot be diffed. + candidates.Sort( + (left, right) => + string.CompareOrdinal(left.AssemblyQualifiedName, right.AssemblyQualifiedName) + ); + + List rules = new List(); + for (int index = 0; index < candidates.Count; index++) + { + Type candidate = candidates[index]; + try + { + if (Activator.CreateInstance(candidate) is IValidationRule rule) + { + rules.Add(rule); + } + } + catch (Exception exception) + { + // Reported rather than thrown: one rule without a parameterless constructor + // would otherwise hide every other rule's findings, and a silent skip would + // report a clean project that nobody had actually checked. + problems?.Add( + candidate.FullName + " could not be constructed: " + exception.Message + ); + } + } + + return rules; + } + + private static ValidationSuppressions ReadSuppressions(string path, List problems) + { + if (string.IsNullOrEmpty(path)) + { + return ValidationSuppressions.Empty; + } + + try + { + return ValidationSuppressions.Parse(File.ReadAllText(path)); + } + catch (Exception exception) + { + // A suppression file that was named and could not be read is not the same as none: + // continuing with an empty set would report every already-accepted finding as new, + // and continuing silently would hide that the file was never applied. + problems?.Add(path + " could not be read: " + exception.Message); + return ValidationSuppressions.Empty; + } + } + + private static bool TryWrite(string path, string contents, out string failure) + { + try + { + string directory = Path.GetDirectoryName(Path.GetFullPath(path)); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(path, contents); + failure = null; + return true; + } + catch (Exception exception) + { + failure = path + " could not be written: " + exception.Message; + return false; + } + } + + /// + /// Reads a severity name, accepting any casing. + /// + /// What the command line said, or null. + /// What to use when it said nothing usable. + /// The severity. + /// + /// An unrecognized name falls back rather than failing. The fallback is the strictest + /// useful threshold, so a typo cannot quietly turn the gate off. + /// + internal static ValidationSeverity ParseSeverity(string value, ValidationSeverity fallback) + { + if (string.IsNullOrEmpty(value)) + { + return fallback; + } + + foreach ( + ValidationSeverity candidate in new[] + { + ValidationSeverity.Info, + ValidationSeverity.Warning, + ValidationSeverity.Error, + } + ) + { + if (string.Equals(value, candidate.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return candidate; + } + } + + return fallback; + } + + /// Reads the value following a named argument, or null. + /// The process arguments. + /// The argument to look for. + /// The first value given for it. + internal static string ValueOf(string[] commandLine, string name) + { + List values = ValuesOf(commandLine, name); + return values.Count == 0 ? null : values[0]; + } + + /// Reads every value given for a named argument, in order. + /// The process arguments. + /// The argument to look for. + /// The values; empty when the argument was not given. + internal static List ValuesOf(string[] commandLine, string name) + { + List values = new List(); + if (commandLine == null) + { + return values; + } + + for (int index = 0; index + 1 < commandLine.Length; index++) + { + if (string.Equals(commandLine[index], name, StringComparison.Ordinal)) + { + values.Add(commandLine[index + 1]); + } + } + + return values; + } + + /// What a headless validation run decided. + public sealed class Result + { + /// + /// Initializes a new instance of the class. + /// + /// The finished run. + /// What the project silences. + /// The rendered report. + /// Anything that went wrong outside a rule. + /// Whether the caller should exit non-zero. + public Result( + ValidationRun run, + ValidationSuppressions suppressions, + string json, + IReadOnlyList problems, + bool failed + ) + { + Run = run; + Suppressions = suppressions; + Json = json; + Problems = problems ?? Array.Empty(); + Failed = failed; + } + + /// The finished run. + public ValidationRun Run { get; } + + /// What the project silences. + public ValidationSuppressions Suppressions { get; } + + /// The rendered JSON report. + public string Json { get; } + + /// Anything that went wrong outside a rule: an unreadable file, an unbuildable rule. + public IReadOnlyList Problems { get; } + + /// Whether the caller should exit non-zero. + public bool Failed { get; } + + /// The exit code the caller should use. + public int ExitCode => Failed ? 1 : 0; + + /// A one-paragraph account of the run, for the console. + public string Summary + { + get + { + int findings = Run == null ? 0 : Run.Findings.Count; + int failures = Run == null ? 0 : Run.Failures.Count; + int considered = Run == null ? 0 : Run.TotalCount; + int unused = + Suppressions == null || Run == null + ? 0 + : Suppressions.UnusedIn(Run.Findings).Count; + + string text = + "Validation: " + + considered + + " asset(s), " + + findings + + " finding(s), " + + failures + + " failure(s), " + + unused + + " unused suppression(s)."; + for (int index = 0; index < Problems.Count; index++) + { + text += "\n " + Problems[index]; + } + + return text; + } + } + } + } +#endif +} diff --git a/Editor/Validation/Continuous/ValidationBatch.cs.meta b/Editor/Validation/Continuous/ValidationBatch.cs.meta new file mode 100644 index 000000000..d794d896d --- /dev/null +++ b/Editor/Validation/Continuous/ValidationBatch.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e5d4735ab259e1b84bcbe86cd991c57 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Validation/Continuous/ValidationReport.cs b/Editor/Validation/Continuous/ValidationReport.cs new file mode 100644 index 000000000..ec1b1f4eb --- /dev/null +++ b/Editor/Validation/Continuous/ValidationReport.cs @@ -0,0 +1,238 @@ +// MIT License - Copyright (c) 2026 wallstop +// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE + +namespace WallstopStudios.UnityHelpers.Editor.Validation.Continuous +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using UnityEngine; + + /// + /// A finished rendered as JSON, for a build that has to decide + /// something about it. + /// + /// + /// + /// The shape is flat and every field is a string or a number, because the consumer is a CI step + /// or another tool rather than this assembly. is written into the + /// document so a consumer can tell an older report from a newer one instead of guessing from + /// which fields happen to be present. + /// + /// + /// Suppressed findings are written with suppressed set rather than dropped. A report + /// that silently omitted them would make a suppression file indistinguishable from a project + /// that had no findings, which is the difference somebody reviewing the file needs to see. + /// + /// + /// Rendered through JsonUtility, the same way every other editor tool here writes JSON, + /// so escaping is Unity's problem rather than a hand-rolled writer's. + /// + /// + public static class ValidationReport + { + /// The schema version written into every document this produces. + public const int SchemaVersion = 1; + + /// + /// Renders a run. + /// + /// The run to render; null yields an empty report. + /// + /// What the project has decided not to be told about; null suppresses nothing. + /// + /// Whether to indent the document. + /// The JSON document; never null. + public static string ToJson( + ValidationRun run, + ValidationSuppressions suppressions, + bool prettyPrint = true + ) + { + ValidationSuppressions effective = suppressions ?? ValidationSuppressions.Empty; + Document document = new Document + { + schemaVersion = SchemaVersion, + assetsConsidered = run == null ? 0 : run.TotalCount, + assetsProcessed = run == null ? 0 : run.ProcessedCount, + complete = run != null && run.IsComplete && !run.IsCancelled, + cancelled = run != null && run.IsCancelled, + }; + + IReadOnlyList findings = + run == null ? Array.Empty() : run.Findings; + for (int index = 0; index < findings.Count; index++) + { + ValidationFinding finding = findings[index]; + bool suppressed = effective.IsSuppressed(finding); + document.findings.Add( + new FindingRecord + { + id = finding.Id, + ruleId = finding.RuleId, + severity = finding.Severity.ToString(), + assetGuid = finding.AssetGuid, + assetPath = finding.AssetPath, + discriminator = finding.Discriminator, + message = finding.Message, + suppressed = suppressed, + } + ); + + if (!suppressed) + { + document.unsuppressedCount++; + } + } + + IReadOnlyList failures = + run == null ? Array.Empty() : run.Failures; + for (int index = 0; index < failures.Count; index++) + { + ValidationRuleFailure failure = failures[index]; + document.failures.Add( + new FailureRecord + { + // A load failure has no rule to blame, and the empty string a JSON reader + // sees for a null is indistinguishable from an unnamed rule -- so the + // report states which it was rather than leaving it to be inferred. + ruleId = failure.RuleId, + loadFailure = failure.IsLoadFailure, + assetPath = failure.AssetPath, + exception = + failure.Exception == null ? string.Empty : failure.Exception.ToString(), + } + ); + } + + IReadOnlyList unused = effective.UnusedIn(findings); + for (int index = 0; index < unused.Count; index++) + { + document.unusedSuppressions.Add(unused[index]); + } + + return JsonUtility.ToJson(document, prettyPrint); + } + + /// + /// Reports whether a run produced anything at or above a severity that is not suppressed. + /// + /// The run to inspect; null counts as nothing found. + /// What to ignore; null suppresses nothing. + /// The lowest severity that counts. + /// true when at least one finding at or above stands. + /// + /// A rule that threw counts, whatever the threshold. It produced no answer for that asset, + /// which is not the same as answering "nothing wrong", so a build that passed on it would + /// be reporting coverage it does not have. + /// + public static bool HasBlockingResults( + ValidationRun run, + ValidationSuppressions suppressions, + ValidationSeverity threshold + ) + { + if (run == null) + { + return false; + } + + if (0 < run.Failures.Count) + { + return true; + } + + ValidationSuppressions effective = suppressions ?? ValidationSuppressions.Empty; + IReadOnlyList findings = run.Findings; + for (int index = 0; index < findings.Count; index++) + { + ValidationFinding finding = findings[index]; + if (threshold <= finding.Severity && !effective.IsSuppressed(finding)) + { + return true; + } + } + + return false; + } + + /// One finding, as the report writes it. + [Serializable] + public sealed class FindingRecord + { + /// The finding's identity across runs. + public string id; + + /// The reporting rule's stable identifier. + public string ruleId; + + /// The severity's name, so the document reads without a lookup table. + public string severity; + + /// The GUID of the asset the finding belongs to. + public string assetGuid; + + /// The asset's project-relative path as of this run. + public string assetPath; + + /// What tells this finding apart from the rule's others on the same asset. + public string discriminator; + + /// The human-readable description. + public string message; + + /// Whether the project's suppression file silences this finding. + public bool suppressed; + } + + /// One rule or loader that threw, as the report writes it. + [Serializable] + public sealed class FailureRecord + { + /// The rule that threw, empty when the asset itself failed to load. + public string ruleId; + + /// Whether loading the asset threw, rather than a rule. + public bool loadFailure; + + /// The asset it was validating. + public string assetPath; + + /// What it threw. + public string exception; + } + + /// The whole document. + [Serializable] + public sealed class Document + { + /// The schema this document follows. + public int schemaVersion; + + /// How many assets the run considered. + public int assetsConsidered; + + /// How many it got through. + public int assetsProcessed; + + /// Whether it reached the end without being cancelled. + public bool complete; + + /// Whether it was cancelled before finishing. + public bool cancelled; + + /// How many findings the suppression file does not silence. + public int unsuppressedCount; + + /// Every finding, suppressed ones included and marked. + public List findings = new List(); + + /// Every rule or loader that threw. + public List failures = new List(); + + /// Suppression entries that silenced nothing in this run. + public List unusedSuppressions = new List(); + } + } +#endif +} diff --git a/Editor/Validation/Continuous/ValidationReport.cs.meta b/Editor/Validation/Continuous/ValidationReport.cs.meta new file mode 100644 index 000000000..5ee336ed6 --- /dev/null +++ b/Editor/Validation/Continuous/ValidationReport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c0669692a748dd39f30b7e68d505637 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Validation/Continuous/ValidationSuppressions.cs b/Editor/Validation/Continuous/ValidationSuppressions.cs new file mode 100644 index 000000000..897c9e2a9 --- /dev/null +++ b/Editor/Validation/Continuous/ValidationSuppressions.cs @@ -0,0 +1,207 @@ +// MIT License - Copyright (c) 2026 wallstop +// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE + +namespace WallstopStudios.UnityHelpers.Editor.Validation.Continuous +{ +#if UNITY_EDITOR + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// The findings a project has decided not to be told about again, read from a committed file. + /// + /// + /// + /// One per line, so the file is a review artifact rather + /// than an opaque blob: a diff shows exactly which check somebody switched off. Blank lines and + /// # comments are ignored, and writes the asset path and message + /// above each entry as a comment, because a rule name and a GUID tell a reviewer nothing. + /// + /// + /// Matching is on the finding's identity -- rule, asset GUID, discriminator -- and never on the + /// path or the message, so moving the asset or rewording the rule does not silently un-suppress + /// it. That is the same identity the finding already documents, and the reason it excludes + /// those two fields. + /// + /// + /// exists because a suppression that outlives the finding it silenced is + /// the same defect class as a linter that cannot report: it reads as a considered decision and + /// is really a stale line nobody has looked at. A headless run reports them rather than letting + /// the file accumulate. + /// + /// + public sealed class ValidationSuppressions + { + private static readonly string[] NoIds = Array.Empty(); + + private static readonly ValidationSuppressions EmptySuppressions = + new ValidationSuppressions( + new List(), + new HashSet(StringComparer.Ordinal) + ); + + private readonly List _ordered; + private readonly HashSet _ids; + + private ValidationSuppressions(List ordered, HashSet ids) + { + _ordered = ordered; + _ids = ids; + } + + /// A set that suppresses nothing. + public static ValidationSuppressions Empty => EmptySuppressions; + + /// How many distinct findings this set suppresses. + public int Count => _ordered.Count; + + /// The suppressed identities, in the order the file listed them. + public IReadOnlyList Ids => _ordered; + + /// + /// Reads a suppression file. + /// + /// The file's contents; null or blank yields . + /// The set; never null. + /// + /// Nothing here throws or reports. A malformed line is a line that suppresses nothing, + /// which the run then reports through along with every other entry + /// that matched nothing -- one mechanism for "this line does not do what you think" rather + /// than a parse error for some shapes and silence for the rest. + /// + public static ValidationSuppressions Parse(string text) + { + if (string.IsNullOrEmpty(text)) + { + return EmptySuppressions; + } + + List ordered = new List(); + HashSet ids = new HashSet(StringComparer.Ordinal); + foreach (string line in text.Replace("\r\n", "\n").Split('\n')) + { + string trimmed = line.Trim(); + if (trimmed.Length == 0 || trimmed[0] == '#') + { + continue; + } + + if (ids.Add(trimmed)) + { + ordered.Add(trimmed); + } + } + + return ordered.Count == 0 + ? EmptySuppressions + : new ValidationSuppressions(ordered, ids); + } + + /// + /// Renders findings as a suppression file, newest decision first in file order. + /// + /// What to suppress; null entries are skipped. + /// The complete file text, with a trailing newline. + /// + /// The comment above each entry is what makes the file reviewable. It is regenerated from + /// the finding rather than preserved from an earlier file, so it cannot drift into + /// describing something the identity no longer points at. + /// + public static string Render(IReadOnlyList findings) + { + StringBuilder builder = new StringBuilder(); + builder.Append("# Validation suppressions.\n"); + builder.Append("# One finding identity per line: rule|assetGuid|discriminator.\n"); + builder.Append("# Delete a line to be told about that finding again. A line that\n"); + builder.Append("# matches nothing is reported by the headless run rather than kept.\n"); + + HashSet written = new HashSet(StringComparer.Ordinal); + for (int index = 0; index < Safe(findings).Count; index++) + { + ValidationFinding finding = findings[index]; + if (!written.Add(finding.Id)) + { + continue; + } + + builder.Append('\n'); + builder.Append("# "); + builder.Append( + string.IsNullOrEmpty(finding.AssetPath) ? "(no path)" : finding.AssetPath + ); + builder.Append(" -- "); + builder.Append(Single(finding.Message)); + builder.Append('\n'); + builder.Append(finding.Id); + builder.Append('\n'); + } + + return builder.ToString(); + } + + /// Reports whether this set silences a finding. + /// The finding to test. + /// true when the file lists the finding's identity. + public bool IsSuppressed(in ValidationFinding finding) + { + return _ids.Contains(finding.Id); + } + + /// + /// The entries that silenced nothing in a run. + /// + /// Every finding the run produced, suppressed ones included. + /// The unmatched identities, in file order; empty when every entry earned its place. + /// + /// Only meaningful for a run that covered the whole project. A run scoped to one folder + /// will not have seen the assets most entries name, so treating its answer as stale + /// suppressions would delete decisions about assets nobody looked at. + /// + public IReadOnlyList UnusedIn(IReadOnlyList findings) + { + if (_ordered.Count == 0) + { + return NoIds; + } + + HashSet seen = new HashSet(StringComparer.Ordinal); + for (int index = 0; index < Safe(findings).Count; index++) + { + seen.Add(findings[index].Id); + } + + List unused = new List(); + for (int index = 0; index < _ordered.Count; index++) + { + if (!seen.Contains(_ordered[index])) + { + unused.Add(_ordered[index]); + } + } + + return unused.Count == 0 ? NoIds : unused; + } + + private static IReadOnlyList Safe(IReadOnlyList values) + { + return values ?? (IReadOnlyList)Array.Empty(); + } + + /// + /// Flattens a message onto one line, so it cannot become an entry of its own. + /// + /// The finding's message. + /// The message with newlines replaced by spaces. + private static string Single(string message) + { + if (string.IsNullOrEmpty(message)) + { + return "(no message)"; + } + + return message.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' '); + } + } +#endif +} diff --git a/Editor/Validation/Continuous/ValidationSuppressions.cs.meta b/Editor/Validation/Continuous/ValidationSuppressions.cs.meta new file mode 100644 index 000000000..96d694c17 --- /dev/null +++ b/Editor/Validation/Continuous/ValidationSuppressions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c5bf92a149b4b359abe9f244f4249554 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/Validation/ValidationReportingTests.cs b/Tests/Editor/Validation/ValidationReportingTests.cs new file mode 100644 index 000000000..5d4f8ff8b --- /dev/null +++ b/Tests/Editor/Validation/ValidationReportingTests.cs @@ -0,0 +1,492 @@ +// MIT License - Copyright (c) 2026 wallstop +// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE + +namespace WallstopStudios.UnityHelpers.Tests.Editor.Validation +{ + using System; + using System.Collections.Generic; + using NUnit.Framework; + using UnityEngine; + using WallstopStudios.UnityHelpers.Editor.Validation.Continuous; + using WallstopStudios.UnityHelpers.Tests.Core; + using Object = UnityEngine.Object; + + /// + /// Pins the headless half of the validation engine: what a suppression file means, what the + /// JSON report says, and what makes a batch run exit non-zero. + /// + /// + /// Everything here is driven from constructed findings and an injected loader rather than from + /// the asset database, so the assertions are about the reporting contract rather than about + /// whatever assets the test project happens to hold. + /// + [TestFixture] + public sealed class ValidationReportingTests : CommonTestBase + { + private const string FirstGuid = "00000000000000000000000000000001"; + private const string SecondGuid = "00000000000000000000000000000002"; + + [Test] + public void ParsingIgnoresBlankLinesCommentsAndDuplicates() + { + ValidationSuppressions suppressions = ValidationSuppressions.Parse( + "# a comment\n\n Rule|" + + FirstGuid + + "|\n" + + " # an indented comment\n" + + "Rule|" + + FirstGuid + + "|\n" + ); + + CollectionAssert.AreEqual(new[] { "Rule|" + FirstGuid + "|" }, suppressions.Ids); + Assert.AreEqual(1, suppressions.Count); + } + + [TestCase(null)] + [TestCase("")] + [TestCase("# nothing but a comment\n")] + public void AFileWithNoEntriesSuppressesNothing(string text) + { + ValidationSuppressions suppressions = ValidationSuppressions.Parse(text); + + Assert.AreEqual(0, suppressions.Count); + Assert.IsFalse(suppressions.IsSuppressed(Finding("Rule", FirstGuid, null))); + } + + [Test] + public void SuppressionSurvivesAMoveAndAReword() + { + // The identity excludes the path and the message precisely so this holds. A suppression + // that came back the moment somebody moved an asset would be worse than none, because + // the reader would believe the decision had been made. + ValidationSuppressions suppressions = ValidationSuppressions.Parse( + ValidationSuppressions.Render( + new List + { + Finding("Rule", FirstGuid, null, "Assets/Old.asset", "the old wording"), + } + ) + ); + + Assert.IsTrue( + suppressions.IsSuppressed( + Finding("Rule", FirstGuid, null, "Assets/New/Moved.asset", "reworded entirely") + ) + ); + } + + [Test] + public void SuppressionDoesNotCrossRulesAssetsOrDiscriminators() + { + ValidationSuppressions suppressions = ValidationSuppressions.Parse( + ValidationSuppressions.Render( + new List { Finding("Rule", FirstGuid, "field") } + ) + ); + + Assert.IsTrue(suppressions.IsSuppressed(Finding("Rule", FirstGuid, "field"))); + Assert.IsFalse(suppressions.IsSuppressed(Finding("Other", FirstGuid, "field"))); + Assert.IsFalse(suppressions.IsSuppressed(Finding("Rule", SecondGuid, "field"))); + Assert.IsFalse(suppressions.IsSuppressed(Finding("Rule", FirstGuid, "otherField"))); + } + + [Test] + public void ARenderedFileNamesTheAssetAndMessageForAReviewer() + { + // A rule name and a GUID tell a reviewer nothing about what is being switched off, and + // the file exists to be reviewed. The comment is not decoration. + string rendered = ValidationSuppressions.Render( + new List + { + Finding("Rule", FirstGuid, null, "Assets/Audio/Theme.wav", "not streaming"), + } + ); + + StringAssert.Contains("Assets/Audio/Theme.wav", rendered); + StringAssert.Contains("not streaming", rendered); + Assert.AreEqual(1, ValidationSuppressions.Parse(rendered).Count); + } + + [Test] + public void ARenderedFileFlattensAMultiLineMessageOntoItsComment() + { + // A message carrying a newline would otherwise put its own second line into the file as + // an entry, which then suppresses nothing and reads as a decision somebody made. + string rendered = ValidationSuppressions.Render( + new List + { + Finding("Rule", FirstGuid, null, "Assets/A.asset", "first\nsecond"), + } + ); + + CollectionAssert.AreEqual( + new[] { "Rule|" + FirstGuid + "|" }, + ValidationSuppressions.Parse(rendered).Ids + ); + } + + [Test] + public void AnEntryThatMatchesNothingIsReported() + { + // A suppression that outlives its finding reads as a considered decision and is really + // a line nobody has looked at, so the run says so rather than letting the file grow. + ValidationSuppressions suppressions = ValidationSuppressions.Parse( + "Rule|" + FirstGuid + "|\nGone|" + SecondGuid + "|\nnot even an id\n" + ); + + CollectionAssert.AreEqual( + new[] { "Gone|" + SecondGuid + "|", "not even an id" }, + suppressions.UnusedIn( + new List { Finding("Rule", FirstGuid, null) } + ) + ); + } + + [Test] + public void TheReportKeepsASuppressedFindingAndMarksIt() + { + // Dropping it would make a project with a suppression file indistinguishable from one + // with nothing wrong, which is the difference a reviewer needs to see. + ValidationRun run = RunOver( + Finding("Rule", FirstGuid, null, "Assets/A.asset", "silenced"), + Finding("Rule", SecondGuid, null, "Assets/B.asset", "loud") + ); + ValidationSuppressions suppressions = ValidationSuppressions.Parse( + "Rule|" + FirstGuid + "|" + ); + + ValidationReport.Document document = Read(ValidationReport.ToJson(run, suppressions)); + + Assert.AreEqual(ValidationReport.SchemaVersion, document.schemaVersion); + Assert.AreEqual(1, document.unsuppressedCount); + Assert.AreEqual(2, document.findings.Count); + Assert.IsTrue( + document.findings.Exists(record => + record.suppressed && record.message == "silenced" + ) + ); + Assert.IsTrue( + document.findings.Exists(record => !record.suppressed && record.message == "loud") + ); + } + + [Test] + public void TheReportSurvivesANullRunAndNullSuppressions() + { + // The batch path renders whatever it got. A report generator that threw on an empty + // project would fail the build for the one state that is unambiguously fine. + ValidationReport.Document document = Read(ValidationReport.ToJson(null, null)); + + Assert.AreEqual(0, document.assetsConsidered); + Assert.AreEqual(0, document.unsuppressedCount); + Assert.IsEmpty(document.findings); + Assert.IsEmpty(document.failures); + } + + [Test] + public void TheReportEscapesAMessageThatWouldBreakTheDocument() + { + // Rendered through JsonUtility precisely so this is Unity's problem rather than a + // hand-rolled writer's, and asserted so a later "simplification" cannot take it away. + ValidationRun run = RunOver( + Finding("Rule", FirstGuid, null, "Assets/A.asset", "he said \"stop\"\nthen \\left") + ); + + // Round-tripped rather than pattern-matched: a document that reads back with the exact + // message is the property, and a check for a backslash would pass on a document no + // reader could parse. + ValidationReport.Document document = Read( + ValidationReport.ToJson(run, ValidationSuppressions.Empty) + ); + + Assert.AreEqual(1, document.findings.Count); + Assert.AreEqual("he said \"stop\"\nthen \\left", document.findings[0].message); + } + + [TestCase(ValidationSeverity.Info, ValidationSeverity.Warning, false)] + [TestCase(ValidationSeverity.Warning, ValidationSeverity.Warning, true)] + [TestCase(ValidationSeverity.Error, ValidationSeverity.Warning, true)] + [TestCase(ValidationSeverity.Error, ValidationSeverity.Error, true)] + [TestCase(ValidationSeverity.Warning, ValidationSeverity.Error, false)] + public void OnlyFindingsAtOrAboveTheThresholdBlock( + ValidationSeverity found, + ValidationSeverity threshold, + bool expected + ) + { + ValidationRun run = RunOver( + Finding("Rule", FirstGuid, null, "Assets/A.asset", "message", found) + ); + + Assert.AreEqual( + expected, + ValidationReport.HasBlockingResults(run, ValidationSuppressions.Empty, threshold) + ); + } + + [Test] + public void ASuppressedFindingDoesNotBlock() + { + ValidationRun run = RunOver( + Finding("Rule", FirstGuid, null, "Assets/A.asset", "message") + ); + + Assert.IsFalse( + ValidationReport.HasBlockingResults( + run, + ValidationSuppressions.Parse("Rule|" + FirstGuid + "|"), + ValidationSeverity.Info + ) + ); + } + + [Test] + public void ARuleThatThrewBlocksWhateverTheThresholdIs() + { + // It produced no answer for that asset, which is not the same as answering "nothing + // wrong". A build that passed on it would be reporting coverage the run does not have. + ValidationRun run = new ValidationRun( + new List { new ThrowingRule() }, + new List + { + new ValidationTarget(FirstGuid, "Assets/A.asset", typeof(ScriptableObject)), + }, + Never + ); + while (!run.Step(double.MaxValue)) { } + + Assert.IsEmpty(run.Findings); + Assert.AreEqual(1, run.Failures.Count); + Assert.IsTrue( + ValidationReport.HasBlockingResults( + run, + ValidationSuppressions.Empty, + ValidationSeverity.Error + ) + ); + ValidationReport.Document document = Read(ValidationReport.ToJson(run, null)); + Assert.AreEqual(1, document.failures.Count); + Assert.IsFalse(document.failures[0].loadFailure, "a rule threw, not the loader"); + Assert.AreEqual("Tests.Throwing", document.failures[0].ruleId); + } + + [TestCase(null, ValidationSeverity.Error)] + [TestCase("", ValidationSeverity.Error)] + [TestCase("nonsense", ValidationSeverity.Error)] + [TestCase("warning", ValidationSeverity.Warning)] + [TestCase("WARNING", ValidationSeverity.Warning)] + [TestCase("Info", ValidationSeverity.Info)] + public void AnUnrecognizedThresholdFallsBackToTheStrictOne( + string written, + ValidationSeverity expected + ) + { + // A typo must not quietly turn the gate off, so the fallback is the strict end. + Assert.AreEqual( + expected, + ValidationBatch.ParseSeverity(written, ValidationSeverity.Error) + ); + } + + [Test] + public void CommandLineValuesAreReadInOrderAndTolerateATrailingFlag() + { + string[] commandLine = + { + "Unity", + ValidationBatch.FolderArgument, + "Assets/A", + ValidationBatch.FolderArgument, + "Assets/B", + ValidationBatch.OutputArgument, + "out.json", + ValidationBatch.SuppressionsArgument, + }; + + CollectionAssert.AreEqual( + new[] { "Assets/A", "Assets/B" }, + ValidationBatch.ValuesOf(commandLine, ValidationBatch.FolderArgument) + ); + Assert.AreEqual( + "out.json", + ValidationBatch.ValueOf(commandLine, ValidationBatch.OutputArgument) + ); + Assert.IsTrue( + ValidationBatch.ValueOf(commandLine, ValidationBatch.SuppressionsArgument) == null, + "a flag with no value after it must not read past the end of the array" + ); + Assert.IsTrue(ValidationBatch.ValueOf(null, ValidationBatch.OutputArgument) == null); + } + + [Test] + public void EveryConstructibleRuleIsFoundInAStableOrder() + { + List problems = new List(); + + List first = ValidationBatch.DiscoverRules(problems); + List second = ValidationBatch.DiscoverRules(null); + + CollectionAssert.AreEqual( + Names(first), + Names(second), + "TypeCache's order is not a property of the project, so discovery has to impose one" + ); + Assert.IsTrue( + first.Exists(rule => + string.Equals(rule.RuleId, "Tests.Throwing", StringComparison.Ordinal) + ), + "this fixture's constructible rule has to be found, or the assertion is vacuous" + ); + } + + [Test] + public void ARuleWithNoParameterlessConstructorIsReportedRatherThanEndingTheRun() + { + // One rule that cannot be built must not hide every other rule's findings, and a silent + // skip would report a clean project nobody had actually checked. ScriptedRule below + // takes its findings as a constructor argument, so it is exactly that shape. + List problems = new List(); + + List rules = ValidationBatch.DiscoverRules(problems); + + Assert.IsTrue( + problems.Exists(problem => problem.Contains(nameof(ScriptedRule))), + "expected the unconstructible rule to be reported, got: " + + string.Join(" | ", problems) + ); + Assert.IsTrue( + rules.Exists(rule => + string.Equals(rule.RuleId, "Tests.Throwing", StringComparison.Ordinal) + ), + "and its neighbours still have to be constructed" + ); + } + + /// + /// Reads a rendered report back, which is what makes the assertions about content rather + /// than about how Unity happens to indent. + /// + /// The rendered document. + /// The parsed document; never null. + private static ValidationReport.Document Read(string json) + { + ValidationReport.Document document = JsonUtility.FromJson( + json + ); + Assert.IsTrue( + document != null, + "the report has to be a document a reader can parse: " + json + ); + return document; + } + + private static string[] Names(List rules) + { + List names = new List(); + for (int index = 0; index < rules.Count; index++) + { + names.Add(rules[index].GetType().FullName); + } + + return names.ToArray(); + } + + private static ValidationFinding Finding( + string ruleId, + string guid, + string discriminator, + string path = "Assets/Asset.asset", + string message = "message", + ValidationSeverity severity = ValidationSeverity.Error + ) + { + return new ValidationFinding( + ruleId, + severity, + null, + guid, + path, + discriminator, + message + ); + } + + /// + /// A finished run whose findings are exactly those given. + /// + /// What the run should report. + /// The completed run. + private static ValidationRun RunOver(params ValidationFinding[] findings) + { + List targets = new List + { + new ValidationTarget(FirstGuid, "Assets/Only.asset", typeof(ScriptableObject)), + }; + ValidationRun run = new ValidationRun( + new List { new ScriptedRule(findings) }, + targets, + Never + ); + while (!run.Step(double.MaxValue)) { } + + return run; + } + + private static Object Never(ValidationTarget target) + { + return null; + } + + /// A rule that reports whatever the fixture handed it, once. + private sealed class ScriptedRule : IValidationRule + { + private readonly ValidationFinding[] _findings; + + internal ScriptedRule(ValidationFinding[] findings) + { + _findings = findings; + } + + public string RuleId => "Tests.Scripted"; + + public string DisplayName => "Scripted"; + + public bool AppliesTo(in ValidationTarget target) + { + return true; + } + + public void Validate( + in ValidationTarget target, + Object asset, + List findings + ) + { + findings.AddRange(_findings); + } + } + + /// A rule that throws, so a failure can be asserted without an asset. + private sealed class ThrowingRule : IValidationRule + { + public string RuleId => "Tests.Throwing"; + + public string DisplayName => "Throwing"; + + public bool AppliesTo(in ValidationTarget target) + { + return true; + } + + public void Validate( + in ValidationTarget target, + Object asset, + List findings + ) + { + throw new InvalidOperationException("rule failed"); + } + } + } +} diff --git a/Tests/Editor/Validation/ValidationReportingTests.cs.meta b/Tests/Editor/Validation/ValidationReportingTests.cs.meta new file mode 100644 index 000000000..142c8ba68 --- /dev/null +++ b/Tests/Editor/Validation/ValidationReportingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e8747f3b4d49ab987ae2164fcce6d4c1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/docs/features/editor-tools/asset-validation.md b/docs/features/editor-tools/asset-validation.md index 3b784479d..81114e464 100644 --- a/docs/features/editor-tools/asset-validation.md +++ b/docs/features/editor-tools/asset-validation.md @@ -135,10 +135,63 @@ if (ValidationSeverity.Warning <= finding.Severity) } ``` +## Run it in CI + +Continuous checks only become a guarantee when something other than a person runs them. One +`-executeMethod` runs every rule in the project and exits non-zero when anything stands: + +```bash +Unity -batchmode -quit -projectPath "$PWD" \ + -executeMethod WallstopStudios.UnityHelpers.Editor.Validation.Continuous.ValidationBatch.ValidateFromCommandLine \ + -validationOutput validation.json \ + -validationSuppressions ValidationSuppressions.txt \ + -validationFailOn Warning +``` + +| Argument | Effect | +| ------------------------- | --------------------------------------------------------------- | +| `-validationOutput` | Where to write the JSON report. Omit it and nothing is written. | +| `-validationSuppressions` | The suppression file to apply. Omit it and nothing is silenced. | +| `-validationFailOn` | Lowest severity that fails the run. Defaults to `Error`. | +| `-validationFolder` | Restrict the run to a folder. Repeat it for several. | + +Rules are found through `TypeCache` and built with their parameterless constructor, in a stable +order so two machines produce the same report. A rule that cannot be constructed is reported and +skipped — one broken rule must not hide every other rule's findings — and the run still fails. + +**A rule that threw fails the run whatever the threshold.** It produced no answer for that asset, +which is not the same as answering "nothing wrong", so passing on it would report coverage the run +does not have. + +The report carries a `schemaVersion`, the counts, every finding (suppressed ones included and +marked), every failure, and any suppression entry that matched nothing. + +## Suppressions + +A suppression file is one finding identity per line, so a diff shows exactly which check somebody +switched off: + +```text +# Assets/Audio/Theme.wav -- 42.0s clip is not streaming. +MyGame.ClipsMustStream|8f3a5c1d9e2b4a7f8c3d6e1a0b5f4c2d| +``` + +`ValidationSuppressions.Render(findings)` writes one, comments and all. `#` lines and blanks are +ignored, so the comment above each entry is regenerated from the finding rather than parsed. + +Matching is on the finding's identity — rule, asset GUID, discriminator — never the path and never +the message. **Moving the asset or rewording the rule does not un-suppress it.** That is the same +identity findings already have, and the reason it excludes those two fields. + +A run reports entries that matched nothing, in the report's `unusedSuppressions` and in the console +summary. A suppression that outlives the finding it silenced reads as a considered decision and is +really a line nobody has looked at. Only trust that list from a run that covered the whole project: +a run scoped to one folder never saw the assets the other entries name. + ## Not yet -This is the engine, not the whole feature. There is no results window, no suppression that survives -a domain reload, no automatic re-run when an asset changes, and no Test Runner adapter — all tracked -on [issue #288](https://github.com/Ambiguous-Interactive/unity-helpers/issues/288). Scenes and -prefab contents are out of scope for now: a run walks assets, and opening a scene to validate it -needs dirty/open/save semantics that are not settled. +This is the engine and its headless reporting, not the whole feature. There is no results window and +no automatic re-run when an asset changes — both tracked on +[issue #288](https://github.com/Ambiguous-Interactive/unity-helpers/issues/288). Scenes and prefab +contents are out of scope for now: a run walks assets, and opening a scene to validate it needs +dirty/open/save semantics that are not settled. From 7dc2774d3268842c1c9d7ae3a3f4271b96221a2c Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 01:14:49 +0000 Subject: [PATCH 05/15] Record a removed member's field number so nothing can take it again A member's field number is a durable wire contract, and the declaration that spends one is deleted along with the member it sits on. WPROTO002 refuses two members claiming one number at the same TIME, so it cannot see a number a deletion freed: delete Health at 3, add a string at 3, and every payload written by an older build reads that field back as a string. No diagnostic, no exception -- the same defect as #606, one level up, reaching far more code because every contract has members. [WProtoReserved(3)] records the removal, and [WProtoReserved("Health")] records the name. Names matter for the reason protobuf reserves both: a re-added Health at a DIFFERENT number still breaks anything matching by name -- a JSON projection, a generated .proto consumer, a schema registry -- while carrying data that means something else. WPROTO043 refuses a member that takes either. It is deliberately one diagnostic, not two: "a new member took a dead number" and "a reservation contradicts a live member" are the same state seen from two sides, nothing in the compiler can tell which is wrong, and a second code could never fire alongside the first. The message names both fixes instead. The schema exporter emits the matching proto3 reserved lines. Without them the exported schema permits, in the consumer's own toolchain, exactly the reuse this refuses. A reserved number outside proto3's range is dropped with a diagnostic naming it, because a schema nobody can parse is worse than one missing a reservation. Reservations are per contract. A base's does not bind its subtypes: their numbers live in a different space, so inheriting one would refuse a member for a collision that cannot happen. Verified: 700 generator tests against protobuf-net 3.2.56 and 699 against 2.4.9. The schema fixtures live in Tests/Runtime, which is excluded from the dotnet harness, so their exact expected text was produced by compiling the real WProtoSchemaText against the same fixtures in a scratch project rather than guessed. The shipped analyzer is rebuilt and byte-identical. Fixes #608 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + .../DiagnosticTests.cs | 152 +++++++++++++++++ .../ReservedMap.cs | 160 ++++++++++++++++++ .../WProtoDiagnostics.cs | 27 +++ .../WProtoGenerator.cs | 22 +++ ...opStudios.UnityHelpers.Proto.Generator.dll | Bin 195584 -> 199680 bytes .../WallstopProto/WProtoReservedAttribute.cs | 100 +++++++++++ .../WProtoReservedAttribute.cs.meta | 11 ++ .../WallstopProto/WProtoSchemaText.cs | 106 ++++++++++++ .../Serialization/WProtoSchemaTextTests.cs | 76 +++++++++ docs/features/serialization/serialization.md | 41 +++++ 11 files changed, 696 insertions(+) create mode 100644 Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs create mode 100644 Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs create mode 100644 Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index b44004580..76a462c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add an editor validation engine: implement `IValidationRule` and run it across the whole project a few milliseconds per tick, not one thirty-second freeze. Only claimed assets load, and one `-executeMethod` runs it in CI with a JSON report and a reviewable suppression file. See [Asset Validation](./docs/features/editor-tools/asset-validation.md) ([#288](https://github.com/Ambiguous-Interactive/unity-helpers/issues/288)). - Add `[WProtoSubtype(typeof(Base))]`, so a subtype joins a WallstopProto hierarchy without picking a field number. The editor assigns and commits the number on the next reload, and **Assign WallstopProto Subtype Tags** retires a removed one -- hand-numbered or not -- so it is never reused. See [Polymorphism](./docs/features/serialization/serialization.md#polymorphism) ([#587](https://github.com/Ambiguous-Interactive/unity-helpers/issues/587), [#601](https://github.com/Ambiguous-Interactive/unity-helpers/issues/601), [#606](https://github.com/Ambiguous-Interactive/unity-helpers/issues/606)). - Add `Sfc64Random`, the Small Fast Chaotic generator: a published-pedigree 64-bit generator with a very small hot path that answers `NextUlong` in one state advance. See [Random Generators](./docs/features/utilities/random-generators.md) ([#516](https://github.com/Ambiguous-Interactive/unity-helpers/issues/516)). +- Add `[WProtoReserved]`, which records a field number or name a removed `[WProtoMember]` held. Taking one again is a build error rather than a save that reads back as the wrong thing, and the exported schema carries the matching proto3 `reserved` lines. See [Retiring a member](./docs/features/serialization/serialization.md#retiring-a-member) ([#608](https://github.com/Ambiguous-Interactive/unity-helpers/issues/608)). - Add a proto schema exporter: **Tools > Wallstop Studios > Unity Helpers > Proto Schema Exporter** writes `proto3` for your `[WProtoContract]` types, so anything downstream can read your saves. Search and tick the exact types, name a package, and write one file or one per assembly, namespace or type ([#424](https://github.com/Ambiguous-Interactive/unity-helpers/issues/424), [#595](https://github.com/Ambiguous-Interactive/unity-helpers/issues/595)). - Add strict UTF-8 validation to WallstopProto strings and Uri: wire bytes that are not valid UTF-8 refuse the payload as malformed instead of decoding to replacement characters, as proto3 requires ([#580](https://github.com/Ambiguous-Interactive/unity-helpers/issues/580)). - Add `IntMap`, an int-keyed open-addressing map measured at 1.26x–2.19x `Dictionary` on hit-heavy lookups, with no comparer indirection on the lookup path. See [Data Structures](./docs/features/utilities/data-structures.md#intmap-int-keyed-open-addressing-map) ([#578](https://github.com/Ambiguous-Interactive/unity-helpers/issues/578)). diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs index 862fdb74b..d55d11d77 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs @@ -1029,6 +1029,158 @@ public void TwoMembersClaimingOneFieldNumberIsAnError() ); } + /// + /// A member cannot take a field number the contract reserved for a removed one. + /// + /// + /// #608. + /// WPROTO002 fires on two members that exist at once and so cannot see a number a deletion + /// freed: every payload written before the removal still carries that field, and giving it + /// to another member reads those saves back as the wrong thing. + /// + [Test] + public void AMemberCannotTakeAReservedFieldNumber() + { + AssertDiagnostic( + "WPROTO043", + "field number 3", + @"[WProtoContract] [WProtoReserved(3)] public sealed partial class Save + { + [WProtoMember(3)] public string Name; + }" + ); + } + + [Test] + public void AMemberCannotTakeAReservedName() + { + // protobuf reserves names as well as numbers, and for the same reason: a re-added + // Health at a DIFFERENT number still breaks anything matching by name -- a JSON + // projection, a generated .proto consumer, a schema registry. + AssertDiagnostic( + "WPROTO043", + "the name 'Health'", + @"[WProtoContract] [WProtoReserved(""Health"")] public sealed partial class Save + { + [WProtoMember(9)] public int Health; + }" + ); + } + + [Test] + public void AMemberTakingBothAReservedNumberAndNameIsNamedForBoth() + { + AssertDiagnostic( + "WPROTO043", + "field number 3 and the name 'Health'", + @"[WProtoContract] [WProtoReserved(3)] [WProtoReserved(""Health"")] public sealed partial class Save + { + [WProtoMember(3)] public int Health; + }" + ); + } + + [Test] + public void OneDeclarationCanReserveSeveralNumbers() + { + AssertDiagnostic( + "WPROTO043", + "field number 9", + @"[WProtoContract] [WProtoReserved(3, 7, 9)] public sealed partial class Save + { + [WProtoMember(9)] public int Later; + }" + ); + } + + [Test] + public void AReservationDoesNotRefuseTheNumbersAroundIt() + { + // The refusal has to be exactly the reserved set. One that swallowed the numbers beside it + // would push every later member up the number line for no reason, and the numbers it + // skipped would be lost as surely as the reserved one. + CollectionAssert.IsEmpty( + Run( + @"[WProtoContract] [WProtoReserved(3)] [WProtoReserved(""Health"")] public sealed partial class Save + { + [WProtoMember(2)] public int Before; + [WProtoMember(4)] public int After; + [WProtoMember(5)] public int Healthy; + }" + ) + .Select(diagnostic => diagnostic.Id + " " + diagnostic.GetMessage()) + .ToArray() + ); + } + + [Test] + public void AReservationOnOneContractDoesNotBindAnother() + { + // Field numbers live in one type's space. A reservation inherited from a base -- or + // leaking to a sibling -- would refuse a member for a collision that cannot happen. + CollectionAssert.IsEmpty( + Run( + @"[WProtoContract] [WProtoReserved(3)] public partial class Base { [WProtoMember(1)] public int A; } + [WProtoContract] [WProtoSubtype(typeof(Base), 100)] public partial class Sub : Base { [WProtoMember(3)] public int B; } + [WProtoContract] public sealed partial class Unrelated { [WProtoMember(3)] public int C; }" + ) + .Select(diagnostic => diagnostic.Id + " " + diagnostic.GetMessage()) + .ToArray() + ); + } + + [Test] + public void ARemovedMemberComingBackUnchangedIsAllowedOnceItsReservationGoes() + { + // The escape the message names, asserted so it is real: a reservation is a record, not + // a permanent ban on a type ever holding that field again. + CollectionAssert.IsEmpty( + Run( + @"[WProtoContract] public sealed partial class Save + { + [WProtoMember(3)] public int Health; + }" + ) + .Select(diagnostic => diagnostic.Id + " " + diagnostic.GetMessage()) + .ToArray() + ); + } + + [Test] + public void ReservationsDoNotChangeWhatTwoLiveMembersOnOneNumberReport() + { + // The acceptance criterion that the existing duplicate rule is untouched. A contract + // that reserves something unrelated still gets WPROTO002 for its live collision. + AssertDiagnostic( + "WPROTO002", + "Second", + @"[WProtoContract] [WProtoReserved(42)] public sealed partial class Clash + { + [WProtoMember(1)] public int First; + [WProtoMember(1)] public int Second; + }" + ); + } + + [Test] + public void EveryMemberOnAReservedNumberIsToldWhyRatherThanOneBeingCalledADuplicate() + { + // Both are wrong for the same reason, and neither may keep the number, so "you are a + // duplicate of the one above" would send the second author to the wrong fix. + CollectionAssert.AreEqual( + new[] { "WPROTO043", "WPROTO043" }, + Run( + @"[WProtoContract] [WProtoReserved(1)] public sealed partial class Clash + { + [WProtoMember(1)] public int First; + [WProtoMember(1)] public int Second; + }" + ) + .Select(diagnostic => diagnostic.Id) + .ToArray() + ); + } + // Every one of these is a shape a developer would reasonably expect to work, which is why it // has to fail the build with a message rather than silently get no formatter. // diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs new file mode 100644 index 000000000..f96847571 --- /dev/null +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs @@ -0,0 +1,160 @@ +// MIT License - Copyright (c) 2026 wallstop +// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE + +namespace WallstopStudios.UnityHelpers.Proto.Generator +{ + using System; + using System.Collections.Generic; + using Microsoft.CodeAnalysis; + + /// + /// The field numbers and member names one contract's [WProtoReserved] declarations hold + /// against every member of that contract. + /// + /// + /// + /// Read from the contract itself and from nothing else. A reservation is a statement about one + /// type's own field-number space, so inheriting one from a base -- whose numbers live in a + /// different space entirely -- would refuse a member for a collision that cannot happen. + /// + /// + /// The record exists because the declaration that spends a number is deleted along with the + /// member it sits on, so WPROTO002 -- which fires on two LIVE claims -- cannot see a + /// number a deletion freed + /// (#608). + /// + /// + internal sealed class ReservedMap + { + internal const string ReservedAttribute = + "WallstopStudios.UnityHelpers.Core.Serialization.WallstopProto.WProtoReservedAttribute"; + + private static readonly ReservedMap EmptyMap = new ReservedMap( + new HashSet(), + new HashSet(StringComparer.Ordinal) + ); + + private readonly HashSet _numbers; + private readonly HashSet _names; + + private ReservedMap(HashSet numbers, HashSet names) + { + _numbers = numbers; + _names = names; + } + + /// A map for a contract that reserves nothing. + internal static ReservedMap Empty => EmptyMap; + + /// Whether this contract reserves anything at all. + internal bool IsEmpty => _numbers.Count == 0 && _names.Count == 0; + + /// The reserved field numbers, ascending. + internal IEnumerable Numbers + { + get + { + List ordered = new List(_numbers); + ordered.Sort(); + return ordered; + } + } + + /// The reserved member names, in ordinal order. + internal IEnumerable Names + { + get + { + List ordered = new List(_names); + ordered.Sort(StringComparer.Ordinal); + return ordered; + } + } + + /// + /// Indexes one contract's reservations. + /// + /// The contract to read. + /// The map; empty when the contract reserves nothing. + internal static ReservedMap Build(INamedTypeSymbol contract) + { + if (contract == null) + { + return EmptyMap; + } + + HashSet numbers = new HashSet(); + HashSet names = new HashSet(StringComparer.Ordinal); + foreach (AttributeData attribute in contract.GetAttributes()) + { + if ( + attribute.AttributeClass == null + || attribute.AttributeClass.ToDisplayString() != ReservedAttribute + ) + { + continue; + } + + foreach (TypedConstant value in Arguments(attribute)) + { + if (value.Value is int number) + { + numbers.Add(number); + } + else if (value.Value is string name && !string.IsNullOrEmpty(name)) + { + names.Add(name); + } + } + } + + return numbers.Count == 0 && names.Count == 0 + ? EmptyMap + : new ReservedMap(numbers, names); + } + + /// Whether a field number may not be used. + /// The number a member is claiming. + /// true when the contract reserves it. + internal bool ReservesNumber(int fieldNumber) + { + return _numbers.Contains(fieldNumber); + } + + /// Whether a member name may not be used. + /// The name a member is declared under. + /// true when the contract reserves it. + internal bool ReservesName(string memberName) + { + return !string.IsNullOrEmpty(memberName) && _names.Contains(memberName); + } + + /// + /// Flattens one declaration's arguments, whichever overload wrote them. + /// + /// The reservation. + /// Every value it names, arrays expanded. + /// + /// Both constructors are (first, params rest[]), so a declaration arrives as a scalar + /// followed by an array. Written to expand any array it finds rather than to assume that + /// shape, so a later overload cannot silently drop its values. + /// + private static IEnumerable Arguments(AttributeData attribute) + { + foreach (TypedConstant argument in attribute.ConstructorArguments) + { + if (argument.Kind == TypedConstantKind.Array) + { + foreach (TypedConstant element in argument.Values) + { + yield return element; + } + + continue; + } + + yield return argument; + } + } + } +} diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoDiagnostics.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoDiagnostics.cs index ec2f00e3b..318c2afb5 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoDiagnostics.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoDiagnostics.cs @@ -455,6 +455,33 @@ internal static class WProtoDiagnostics isEnabledByDefault: true ); + /// + /// A member claiming a field number or a name the contract has reserved. + /// + /// + /// + /// Its own code rather than part of WPROTO002, whose subject is two members that + /// exist at once. This one is about a member that no longer exists: the declaration that + /// spent the number was deleted with it, so nothing but the reservation records that the + /// number was ever used + /// (#608). + /// + /// + /// It is also the answer to a reservation that contradicts a live member, because that is + /// the same state seen from the other side. Which of the two is wrong cannot be decided + /// here -- the member may be the removed one coming back unchanged -- so the message offers + /// both fixes rather than a second diagnostic that could never fire alongside this one. + /// + /// + internal static readonly DiagnosticDescriptor ReservedTag = new DiagnosticDescriptor( + "WPROTO043", + "WallstopProto member takes something the contract reserved", + "'{0}.{1}' claims {2}, which '{0}' reserves with [WProtoReserved]. A reservation records what a removed member held, because the declaration that spent it was deleted along with it -- so every payload written before the removal still carries that field, and giving it to another member reads those saves back as the wrong thing. Use a free field number and an unreserved name, or, if this really is the removed member coming back unchanged, delete the matching [WProtoReserved] in the same commit.", + "WallstopProto", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + internal static readonly DiagnosticDescriptor HookSignature = new DiagnosticDescriptor( "WPROTO008", "WallstopProto lifecycle hook has the wrong signature", diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs index d79b55c42..f6b6cd72a 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs @@ -3133,6 +3133,7 @@ NestedCollections nested { List members = new List(); Dictionary claimed = new Dictionary(); + ReservedMap reserved = ReservedMap.Build(contract); bool failed = false; foreach (ISymbol symbol in contract.GetMembers()) @@ -3193,6 +3194,27 @@ NestedCollections nested continue; } + // Checked after the duplicate, because a member colliding with a LIVE sibling has a + // fix the author can see in front of them; a collision with something deleted needs + // the reservation explained. + if (reserved.ReservesNumber(tag) || reserved.ReservesName(symbol.Name)) + { + Report( + context, + WProtoDiagnostics.ReservedTag, + symbol, + contract.Name, + symbol.Name, + reserved.ReservesNumber(tag) + ? reserved.ReservesName(symbol.Name) + ? "field number " + tag + " and the name '" + symbol.Name + "'" + : "field number " + tag + : "the name '" + symbol.Name + "'" + ); + failed = true; + continue; + } + bool zigZag = AsksForZigZag(attribute); if (zigZag && !Shape.SupportsZigZag(type)) { diff --git a/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll b/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll index a95dc58f4cebd952a8bd7281c5c67fc4e60ec114..228729599ad43901f80b79a5fe5f9d9cc25efd5b 100644 GIT binary patch literal 199680 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

%d4z<|8k@Fia9Mc2MRe>9Mr?A_sW}1 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 + /// Field numbers, or member names, that a removed [WProtoMember] used to hold and that + /// nothing on this contract may take again. + ///

+ /// + /// + /// A field number is a durable wire contract, and the declaration that spends one is deleted + /// along with the member it sits on. WPROTO002 refuses two members claiming one number at + /// the same TIME and so has no memory: a number freed by a deletion is indistinguishable from + /// one never used, and handing it to a later member reads every payload written by an older + /// build as the wrong type. This is the record that makes the deletion visible, and it is the + /// same mechanism proto3 spells reserved. + /// + /// + /// [WProtoContract] + /// [WProtoReserved(3)] // Health, removed in 4.0 + /// [WProtoReserved(7, 9)] // several at once + /// [WProtoReserved("Health")] // and the name it went by + /// public partial class Player { } + /// + /// + /// Names are reserved as well as numbers, for the reason protobuf reserves both: a re-added + /// Health at a different number still breaks anything that matches by name -- a JSON + /// projection, a generated .proto consumer, a schema registry -- while carrying data that + /// means something else. + /// + /// + /// The record is an attribute rather than a generated manifest because a member number is always + /// written by hand. Nothing assigns one, so the record belongs beside the contract where the + /// next author is already reading, and a reservation that contradicts a live member is refused + /// rather than silently outranking it. + /// + /// + [Preserve] + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Struct, + AllowMultiple = true, + Inherited = false + )] + public sealed class WProtoReservedAttribute : Attribute + { + private static readonly int[] NoNumbers = Array.Empty(); + private static readonly string[] NoNames = Array.Empty(); + + /// + /// Reserves one or more field numbers. + /// + /// A field number no member may take again. + /// Any further numbers to reserve in the same declaration. + /// + /// The first number is separate from the rest so that [WProtoReserved()] cannot + /// compile. An empty reservation reads as a considered decision and records nothing, which + /// is the state this attribute exists to prevent. + /// + public WProtoReservedAttribute(int fieldNumber, params int[] alsoReserved) + { + int[] numbers = new int[1 + (alsoReserved == null ? 0 : alsoReserved.Length)]; + numbers[0] = fieldNumber; + for (int index = 1; index < numbers.Length; index++) + { + numbers[index] = alsoReserved[index - 1]; + } + + FieldNumbers = numbers; + MemberNames = NoNames; + } + + /// + /// Reserves one or more member names. + /// + /// A member name no member may take again. + /// Any further names to reserve in the same declaration. + public WProtoReservedAttribute(string memberName, params string[] alsoReserved) + { + string[] names = new string[1 + (alsoReserved == null ? 0 : alsoReserved.Length)]; + names[0] = memberName; + for (int index = 1; index < names.Length; index++) + { + names[index] = alsoReserved[index - 1]; + } + + MemberNames = names; + FieldNumbers = NoNumbers; + } + + /// The field numbers this declaration holds; empty when it reserves names. + public int[] FieldNumbers { get; } + + /// The member names this declaration holds; empty when it reserves numbers. + public string[] MemberNames { get; } + } +} diff --git a/Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs.meta b/Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs.meta new file mode 100644 index 000000000..ad126e8f3 --- /dev/null +++ b/Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2fa806d44b37035c53cac6b42f49ea3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/Serialization/WallstopProto/WProtoSchemaText.cs b/Runtime/Core/Serialization/WallstopProto/WProtoSchemaText.cs index 092d1e82f..6deb7a6fb 100644 --- a/Runtime/Core/Serialization/WallstopProto/WProtoSchemaText.cs +++ b/Runtime/Core/Serialization/WallstopProto/WProtoSchemaText.cs @@ -393,6 +393,7 @@ public string TryAddContract(Type contractType) List members = CollectMembers(contractType, messageName); StringBuilder body = new StringBuilder(); body.Append("message ").Append(messageName).Append(" {").Append("\n"); + AppendReserved(contractType, messageName, body); foreach (MemberEntry member in members) { string line = RenderField(messageName, member); @@ -425,6 +426,111 @@ public string TryAddContract(Type contractType) return messageName; } + /// + /// Writes the contract's [WProtoReserved] declarations as proto3 reservations. + /// + /// The contract being rendered. + /// Its schema name, for diagnostics. + /// The message body being built. + /// + /// Without these the exported schema permits, in the consumer's own toolchain, exactly + /// the reuse the generator refuses here -- so a removed member's number would come back + /// meaning something else one build system over. + /// + private void AppendReserved(Type contractType, string messageName, StringBuilder body) + { + object[] markers = contractType.GetCustomAttributes( + typeof(WProtoReservedAttribute), + false + ); + if (markers.Length == 0) + { + return; + } + + SortedSet numbers = new SortedSet(); + SortedSet names = new SortedSet(StringComparer.Ordinal); + foreach (object marker in markers) + { + WProtoReservedAttribute reserved = marker as WProtoReservedAttribute; + if (reserved == null) + { + continue; + } + + foreach (int number in reserved.FieldNumbers) + { + // A number proto3 could not have used is not a number this schema can + // reserve: protoc rejects both ends of the range and owns 19000-19999 + // itself, so emitting one would make the whole file impossible to parse. + if ( + 1 <= number + && number <= 536870911 + && (number < 19000 || 19999 < number) + ) + { + numbers.Add(number); + } + else + { + _diagnostics.Add( + $"{messageName}: reserved field number {number.ToString(CultureInfo.InvariantCulture)} is outside the range proto3 allows; omitted." + ); + } + } + + foreach (string name in reserved.MemberNames) + { + if (!string.IsNullOrEmpty(name) && IsValidProtoIdentifier(name)) + { + names.Add(name); + } + else + { + _diagnostics.Add( + $"{messageName}: reserved name '{name}' is not a valid proto3 identifier; omitted." + ); + } + } + } + + if (0 < numbers.Count) + { + body.Append(" reserved "); + bool first = true; + foreach (int number in numbers) + { + if (!first) + { + body.Append(", "); + } + + body.Append(number.ToString(CultureInfo.InvariantCulture)); + first = false; + } + + body.Append(";").Append("\n"); + } + + if (0 < names.Count) + { + body.Append(" reserved "); + bool first = true; + foreach (string name in names) + { + if (!first) + { + body.Append(", "); + } + + body.Append('"').Append(name).Append('"'); + first = false; + } + + body.Append(";").Append("\n"); + } + } + private string RenderField(string ownerName, MemberEntry member) { Type memberType = member.MemberType; diff --git a/Tests/Runtime/Serialization/WProtoSchemaTextTests.cs b/Tests/Runtime/Serialization/WProtoSchemaTextTests.cs index 2c4cd7ca7..bc66a5c52 100644 --- a/Tests/Runtime/Serialization/WProtoSchemaTextTests.cs +++ b/Tests/Runtime/Serialization/WProtoSchemaTextTests.cs @@ -49,6 +49,65 @@ out IReadOnlyList diagnostics Assert.IsEmpty(diagnostics, "A fully supported contract must not report anything."); } + [Test] + public void ReservedNumbersAndNamesReachTheSchema() + { + // Without these the exported schema permits, in the consumer's own toolchain, exactly + // the reuse the generator refuses here (#608) -- so a removed member's number would come + // back meaning something else one build system over. + bool rendered = WProtoSchemaText.TryWriteSchema( + new[] { typeof(SchemaReserved) }, + "test.pkg", + null, + out string schema, + out IReadOnlyList diagnostics + ); + + Assert.IsTrue(rendered); + Assert.AreEqual( + GeneratedHeader + + "package test.pkg;\n" + + "\n" + + "message SchemaReserved {\n" + + " reserved 2, 7;\n" + + " reserved \"Armour\", \"Health\";\n" + + " int32 Kept = 1;\n" + + "}\n" + + "\n", + schema, + "The schema is generated output; its exact text is the contract." + ); + Assert.IsEmpty(diagnostics); + } + + [Test] + public void AReservedNumberProtocCouldNotParseIsOmittedAndReported() + { + // protoc rejects both ends of the field-number range and owns 19000-19999 itself, so + // emitting one would make the whole file impossible to parse -- a schema nobody can read is + // worse than one missing a reservation, and the diagnostic says which was dropped. + bool rendered = WProtoSchemaText.TryWriteSchema( + new[] { typeof(SchemaReservedOutOfRange) }, + "test.pkg", + null, + out string schema, + out IReadOnlyList diagnostics + ); + + Assert.IsTrue(rendered); + StringAssert.DoesNotContain("reserved", schema); + CollectionAssert.AreEqual( + new[] + { + "SchemaReservedOutOfRange: reserved field number 0 is outside the range proto3 allows; omitted.", + "SchemaReservedOutOfRange: reserved field number 19500 is outside the range proto3 allows; omitted.", + "SchemaReservedOutOfRange: reserved field number 536870912 is outside the range proto3 allows; omitted.", + }, + diagnostics, + "each dropped number has to be named, or the author cannot tell which record was lost" + ); + } + [Test] public void ScalarMembersMapToTheirWireTypes() { @@ -870,4 +929,21 @@ public sealed partial class SchemaEnumHost [WProtoMember(1)] public SchemaAliased Value; } + + [WProtoContract] + [WProtoReserved(2, 7)] + [WProtoReserved("Health", "Armour")] + public sealed partial class SchemaReserved + { + [WProtoMember(1)] + public int Kept; + } + + [WProtoContract] + [WProtoReserved(0, 19500, 536870912)] + public sealed partial class SchemaReservedOutOfRange + { + [WProtoMember(1)] + public int Kept; + } } diff --git a/docs/features/serialization/serialization.md b/docs/features/serialization/serialization.md index 70e500d8c..18fcf1772 100644 --- a/docs/features/serialization/serialization.md +++ b/docs/features/serialization/serialization.md @@ -1098,6 +1098,9 @@ everywhere. `AbstractRandom` is the worked example: the after-deserialization wo needs is declared on `AbstractRandom` and dispatched through `OnAfterDeserialization`. Suppress `WPROTO034` at the declaration when the hook only repeats work every other path already does. +`WPROTO043` fires when a member takes a field number, or a name, that the contract reserved. See +[Retiring a member](#retiring-a-member). + `WPROTO039`, `WPROTO040`, `WPROTO041` and `WPROTO042` are specific to declaring a subtype **from the subtype** with `[WProtoSubtype]`. `WPROTO039` fires when two subtypes of one base claim the same field number, whichever end each was declared from, and names both types and the number. @@ -1651,6 +1654,44 @@ subtype round-trips as that subtype. What you give up is being _dispatched as_ a collection declared `List` cannot hold a `PlasmaCutter`. Declare the collection as your own type instead. +#### Retiring a member + +A field number is a durable wire contract, and the declaration that spends one is deleted along with +the member it sits on. `WPROTO002` refuses two members claiming one number at the same **time**, so +it cannot see a number a deletion freed: delete `Health`, add something else at 3, and every payload +written by an older build reads that field back as the wrong thing, with no diagnostic anywhere. + +Record the removal where the next author is already reading: + +```csharp +[WProtoContract] +[WProtoReserved(3)] // Health, removed in 4.0 +[WProtoReserved(7, 9)] // several at once +[WProtoReserved("Health")] // and the name it went by +public partial class Player +{ + [WProtoMember(1)] + public string Name; +} +``` + +A member that takes a reserved number or a reserved name is `WPROTO043`. **Names are reserved as +well as numbers**, for the reason protobuf reserves both: a re-added `Health` at a _different_ number +still breaks anything matching by name -- a JSON projection, a generated `.proto` consumer, a schema +registry -- while carrying data that means something else. + +A reservation is a record, not a permanent ban. If the removed member really is coming back +unchanged, delete the matching `[WProtoReserved]` in the same commit; `WPROTO043`'s message says so, +because from the compiler's side "a new member took a dead number" and "a reservation contradicts a +live member" are the same state and nothing there can tell them apart. + +Reservations are per contract. A base's reservation does not bind its subtypes: their numbers live in +a different space, so inheriting one would refuse a member for a collision that cannot happen. + +The [schema exporter](#exporting-a-proto3-schema) writes them out as proto3 `reserved` lines. Without +that, the exported schema would permit, in a consumer's own toolchain, exactly the reuse this +refuses. + #### Surrogates Unity's `Vector3`, `Color` and `Bounds` cannot carry `[WProtoContract]`; they are not yours to From a0667526e4d6b0bf285fd4a68ba0ba97173bd3fe Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 01:27:10 +0000 Subject: [PATCH 06/15] Refuse a documentation example that names something we do not have lint:code-samples extracts 3,061 C# blocks out of docs/ and validates none of them, so an example naming something moved or renamed reads as correct forever. Session 236 found invented APIs in three pages by hand; nothing stops the next one. Two rules, chosen because they have no false positives. A general "does this API exist" check needs a real parser -- a first attempt keyed on Type.Member reported Serializer.ProtoDeserialize as missing, because a regex over declarations cannot see a generic method, and a gate that cries wolf is one people stop reading. 1. A `using WallstopStudios.UnityHelpers...;` must name a namespace some .cs file declares. A using is unambiguous: it is a namespace, spelled in full, or it does not compile. 2. An `` in a link.xml example must name an assembly some .asmdef declares. Both found a real defect on the run that introduced them, and both shipped in 3.5.1: - The enum display-name example imported Core.Attribute, not Core.Attributes. A reader copying it gets a compile error. - The link.xml example preserved `WallstopStudios.UnityHelpers.Runtime`. The runtime assembly is `WallstopStudios.UnityHelpers`, and the linker reports nothing for a name it cannot match -- so that one surfaced as a stripped player rather than as an error. Ten self-test cases, red halves included, and the run-repo-lint meta-check was verified to catch this linter when its self-test was unregistered. Addresses #441 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + docs/features/serialization/serialization.md | 2 +- .../features/utilities/math-and-extensions.md | 2 +- package.json | 2 + scripts/lint-doc-identifiers.js | 201 +++++++++++++++++ scripts/lint-doc-identifiers.js.meta | 7 + scripts/run-contract-tests.js | 5 + scripts/run-repo-lint.js | 5 + scripts/tests/test-lint-doc-identifiers.js | 208 ++++++++++++++++++ .../tests/test-lint-doc-identifiers.js.meta | 7 + 10 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 scripts/lint-doc-identifiers.js create mode 100644 scripts/lint-doc-identifiers.js.meta create mode 100644 scripts/tests/test-lint-doc-identifiers.js create mode 100644 scripts/tests/test-lint-doc-identifiers.js.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a462c5d..28b18801c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -150,6 +150,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fix two documentation examples that could not work as printed: the enum display-name sample imported `Core.Attribute` rather than `Core.Attributes`, and the `link.xml` sample preserved an assembly name that does not exist, which strips silently ([#441](https://github.com/Ambiguous-Interactive/unity-helpers/issues/441)). - Fix fifteen broken documentation links on `AssetDatabaseBatchScope`: its `` references to `AssetDatabase.Refresh`, `CreateAsset` and `ImportAsset` named an ambiguous overload or an unresolvable type, so an IDE linked the wrong overload or nothing ([#594](https://github.com/Ambiguous-Interactive/unity-helpers/issues/594)). - Fix the IntelliSense tooltip on ten public `ReflectionHelpers` delegate factories, which carried two `` tags and showed the vaguer one ([#441](https://github.com/Ambiguous-Interactive/unity-helpers/issues/441)). - Fix zero-valued `ValueTuple` components and fixed-width map keys being omitted by WallstopProto where protobuf-net writes them explicitly, including enum tuple map keys ([#399](https://github.com/Ambiguous-Interactive/unity-helpers/issues/399)). diff --git a/docs/features/serialization/serialization.md b/docs/features/serialization/serialization.md index 18fcf1772..0b3fc829c 100644 --- a/docs/features/serialization/serialization.md +++ b/docs/features/serialization/serialization.md @@ -459,7 +459,7 @@ In your `Assets` folder (or any subfolder), create `link.xml` to preserve your P - + diff --git a/docs/features/utilities/math-and-extensions.md b/docs/features/utilities/math-and-extensions.md index a2f9bda76..412b57338 100644 --- a/docs/features/utilities/math-and-extensions.md +++ b/docs/features/utilities/math-and-extensions.md @@ -1075,7 +1075,7 @@ gets a dictionary. Negative members count normally toward that span, so **The problem:** Enum values often need different names in UI than in code. ```csharp -using WallstopStudios.UnityHelpers.Core.Attribute; +using WallstopStudios.UnityHelpers.Core.Attributes; public enum Difficulty { diff --git a/package.json b/package.json index ff5456cb2..61162de2f 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "lint:docs": "node ./scripts/run-doc-link-lint.js", "lint:doc-links": "node ./scripts/run-doc-link-lint.js --verbose", "lint:code-samples": "node ./scripts/extract-code-samples.js --extract-only", + "lint:doc-identifiers": "node ./scripts/lint-doc-identifiers.js", "lint:code-samples:verbose": "node ./scripts/extract-code-samples.js --verbose --extract-only", "lint:spelling": "node ./scripts/run-node-bin.js cspell --no-progress --show-suggestions", "lint:spelling:verbose": "node ./scripts/run-node-bin.js cspell --show-suggestions", @@ -203,6 +204,7 @@ "test:validate-hook-sync-calls": "pwsh -NoProfile -File scripts/tests/test-validate-hook-sync-calls.ps1", "test:validate-github-pages-css": "bash scripts/tests/test-validate-github-pages-css.sh", "test:check-code-fence-syntax": "bash scripts/tests/test-check-code-fence-syntax.sh", + "test:lint-doc-identifiers": "node scripts/tests/test-lint-doc-identifiers.js", "test:lint-dependabot": "pwsh -NoProfile -File scripts/tests/test-lint-dependabot.ps1 -VerboseOutput", "test:lint-duplicate-usings": "pwsh -NoProfile -File scripts/tests/test-lint-duplicate-usings.ps1 -VerboseOutput", "test:lint-preserve-attributes": "pwsh -NoProfile -File scripts/tests/test-lint-preserve-attributes.ps1 -VerboseOutput", diff --git a/scripts/lint-doc-identifiers.js b/scripts/lint-doc-identifiers.js new file mode 100644 index 000000000..573dd66e9 --- /dev/null +++ b/scripts/lint-doc-identifiers.js @@ -0,0 +1,201 @@ +#!/usr/bin/env node +/** + * Documentation may not name a namespace or an assembly this repository does not have. + * + * `lint:code-samples` extracts 3,061 C# blocks out of `docs/` and validates none of them, so an + * example that names something moved or renamed reads as correct forever. The damage is silent + * twice over: the reader copies it, and the person who moved the namespace gets no signal at all. + * + * TWO RULES, both chosen because they have no false positives. A general "does this API exist" + * check needs a real parser -- a first attempt keyed on `Type.Member` reported + * `Serializer.ProtoDeserialize` as missing, because a regex over declarations cannot see a generic + * method -- and a gate that cries wolf is one people stop reading: + * + * 1. Every `using WallstopStudios.UnityHelpers...;` in a Markdown file must name a namespace some + * `.cs` file under the governed source roots declares. A `using` is unambiguous: it is a + * namespace, spelled in full, or it does not compile. + * 2. Every `` in a `link.xml` example must name an + * assembly some `.asmdef` declares. A wrong name here is worse than a compile error, because + * the linker silently preserves nothing and the failure arrives as a stripped player. + * + * Both found a real defect on the run that introduced them: `Core.Attribute` for `Core.Attributes` + * in the enum display-name example, and `WallstopStudios.UnityHelpers.Runtime` for the runtime + * assembly, which is named `WallstopStudios.UnityHelpers` + * ([#441](https://github.com/Ambiguous-Interactive/unity-helpers/issues/441)). + * + * Exit codes: 0 = every name resolves, 1 = at least one does not. + */ + +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const REPO_ROOT = path.resolve(__dirname, ".."); +// Overridable so the self-test can point the scan at a fixture tree. Nothing in CI sets it, so the +// default is the only path that ships. +const SCAN_ROOT = process.env.DOC_IDENTIFIER_ROOT + ? path.resolve(process.env.DOC_IDENTIFIER_ROOT) + : REPO_ROOT; + +/** Where a namespace or an assembly may be declared. */ +const SOURCE_ROOTS = ["Runtime", "Editor", "Tests", "Generator~", "Samples~"]; + +/** Where documentation is read from. */ +const DOC_ROOTS = ["docs"]; + +const SKIPPED_DIRECTORIES = new Set(["obj", "bin", "node_modules", "Library", "artifacts"]); + +const USING_PATTERN = /^\s*using\s+(WallstopStudios(?:\.[A-Za-z0-9_]+)*)\s*;/; +const ASSEMBLY_PATTERN = / entry.name.endsWith(extension))) { + found.push(path.join(current, entry.name)); + } + } + } + + return found; +} + +/** + * The namespaces and assembly names this repository actually declares. + * + * @param {string} root Repository root to read. + * @returns {{namespaces: Set, assemblies: Set}} What exists. + */ +function declared(root) { + const namespaces = new Set(); + const assemblies = new Set(); + + for (const sourceRoot of SOURCE_ROOTS) { + for (const file of filesUnder(path.join(root, sourceRoot), [".cs"])) { + const text = fs.readFileSync(file, "utf8"); + NAMESPACE_PATTERN.lastIndex = 0; + let match; + while ((match = NAMESPACE_PATTERN.exec(text)) !== null) { + // Every ancestor too: `using A.B;` is legal wherever `A.B.C` is declared. + const parts = match[1].split("."); + for (let length = 1; length <= parts.length; length++) { + namespaces.add(parts.slice(0, length).join(".")); + } + } + } + + for (const file of filesUnder(path.join(root, sourceRoot), [".asmdef"])) { + try { + const name = JSON.parse(fs.readFileSync(file, "utf8")).name; + if (typeof name === "string" && 0 < name.length) { + assemblies.add(name); + } + } catch { + // An unreadable asmdef is lint-asmdef's subject, not this one's. Skipping it here can only + // produce a false positive further down, which the report names precisely enough to see. + } + } + } + + return { namespaces, assemblies }; +} + +/** + * Checks every documentation file against what the sources declare. + * + * @param {string} root Repository root to scan. + * @returns {{violations: string[], usings: number, assemblies: number}} What was checked and what failed. + */ +function analyze(root) { + const { namespaces, assemblies } = declared(root); + const violations = []; + let usingCount = 0; + let assemblyCount = 0; + + for (const docRoot of DOC_ROOTS) { + for (const file of filesUnder(path.join(root, docRoot), [".md"])) { + const relative = path.relative(root, file).split(path.sep).join("/"); + const lines = fs.readFileSync(file, "utf8").split("\n"); + lines.forEach((line, index) => { + const usingMatch = line.match(USING_PATTERN); + if (usingMatch !== null) { + usingCount++; + if (!namespaces.has(usingMatch[1])) { + violations.push( + `${relative}:${index + 1}: 'using ${usingMatch[1]};' names a namespace nothing declares. ` + + `A reader copying this example gets a compile error.` + ); + } + } + + ASSEMBLY_PATTERN.lastIndex = 0; + let assemblyMatch; + while ((assemblyMatch = ASSEMBLY_PATTERN.exec(line)) !== null) { + assemblyCount++; + if (!assemblies.has(assemblyMatch[1])) { + violations.push( + `${relative}:${index + 1}: names an assembly ` + + `no .asmdef declares. The linker preserves nothing under a wrong name and reports ` + + `nothing, so this surfaces as a stripped player rather than as an error.` + ); + } + } + }); + } + } + + return { violations, usings: usingCount, assemblies: assemblyCount }; +} + +function main() { + const { violations, usings, assemblies } = analyze(SCAN_ROOT); + + if (0 < violations.length) { + console.error( + `[lint-doc-identifiers] ${violations.length} documentation reference(s) do not resolve:` + ); + for (const violation of violations) { + console.error(` ${violation}`); + } + + process.exitCode = 1; + return; + } + + // The counts are the gate's own red half at a glance: a scan that checked nothing would say so + // here rather than printing the same success line a clean corpus does. + console.log( + `[lint-doc-identifiers] ${usings} package using directive(s) and ${assemblies} assembly ` + + `reference(s) in docs all resolve.` + ); +} + +if (require.main === module) { + main(); +} + +module.exports = { analyze, declared, filesUnder, SOURCE_ROOTS, DOC_ROOTS }; diff --git a/scripts/lint-doc-identifiers.js.meta b/scripts/lint-doc-identifiers.js.meta new file mode 100644 index 000000000..04ccaed0b --- /dev/null +++ b/scripts/lint-doc-identifiers.js.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b2e9994f3221ab919a3f1d524733e7a4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/scripts/run-contract-tests.js b/scripts/run-contract-tests.js index 5372eaea7..b18d6b8e4 100644 --- a/scripts/run-contract-tests.js +++ b/scripts/run-contract-tests.js @@ -111,6 +111,11 @@ const CHECKS = [ name: "Code fence syntax gate self-test", run: "npm run test:check-code-fence-syntax" }, + { + id: "lint-doc-identifiers", + name: "Documentation identifier linter self-test", + run: "npm run test:lint-doc-identifiers" + }, { id: "lint-dependabot", name: "Dependabot linter self-test", diff --git a/scripts/run-repo-lint.js b/scripts/run-repo-lint.js index bdba90ed9..57b8bccee 100644 --- a/scripts/run-repo-lint.js +++ b/scripts/run-repo-lint.js @@ -215,6 +215,11 @@ const CHECKS = [ name: "Documentation code samples", run: "npm run lint:code-samples" }, + { + id: "doc-identifiers", + name: "Documentation namespace and assembly names", + run: "npm run lint:doc-identifiers" + }, // Gaps found while consolidating: these four are in `validate:content` / `validate:local` and so // ran on a developer's machine, but no workflow invoked any of them. They pass today; the point // is that nothing would have said so if they stopped. diff --git a/scripts/tests/test-lint-doc-identifiers.js b/scripts/tests/test-lint-doc-identifiers.js new file mode 100644 index 000000000..34ab6a400 --- /dev/null +++ b/scripts/tests/test-lint-doc-identifiers.js @@ -0,0 +1,208 @@ +#!/usr/bin/env node +/** + * Self-test for scripts/lint-doc-identifiers.js. + * + * Run against the repository the linter prints a success line, which is evidence about `docs/` and + * no evidence that it still reports (#556). Every case below drives it over a fixture tree through + * `DOC_IDENTIFIER_ROOT`, so each rule has a red half that fails the suite if the rule stops firing. + * + * Green halves also matter here more than usual, because the whole design claim is "no false + * positives": a `using` of a parent namespace, of a namespace declared only under `Generator~`, and + * a non-package `using` all have to pass. + */ + +"use strict"; + +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const REPO_ROOT = path.resolve(__dirname, "..", ".."); +const LINTER = path.join(REPO_ROOT, "scripts", "lint-doc-identifiers.js"); + +let passed = 0; +let failed = 0; +const failedTests = []; + +function test(name, body) { + try { + body(); + console.log(` [PASS] ${name}`); + passed++; + } catch (error) { + console.log(` [FAIL] ${name}`); + console.log(` ${error.message}`); + failed++; + failedTests.push(name); + } +} + +/** + * Builds a scratch repository and runs the linter over it. + * + * @param {{sources: Record, docs: Record}} tree What to write. + * @returns {{status: number, output: string}} The linter's exit code and combined output. + */ +function run(tree) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "lint-doc-identifiers-")); + try { + for (const [relative, contents] of Object.entries(tree.sources || {})) { + const full = path.join(root, relative); + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(full, contents); + } + + for (const [relative, contents] of Object.entries(tree.docs || {})) { + const full = path.join(root, "docs", relative); + fs.mkdirSync(path.dirname(full), { recursive: true }); + fs.writeFileSync(full, contents); + } + + const result = spawnSync(process.execPath, [LINTER], { + encoding: "utf8", + env: { ...process.env, DOC_IDENTIFIER_ROOT: root } + }); + + return { + status: result.status, + output: `${result.stdout || ""}${result.stderr || ""}` + }; + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +const NAMESPACE_SOURCE = { + "Runtime/Core/Attributes/Thing.cs": + "namespace WallstopStudios.UnityHelpers.Core.Attributes\n{\n public sealed class Thing { }\n}\n", + "Runtime/WallstopStudios.UnityHelpers.asmdef": '{\n "name": "WallstopStudios.UnityHelpers"\n}\n' +}; + +console.log("\nTesting scripts/lint-doc-identifiers.js...\n"); + +// -- Green half --------------------------------------------------------------- +test("a using that names a declared namespace passes", () => { + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { + "guide.md": "```csharp\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n```\n" + } + }); + + assert.strictEqual(status, 0, output); + assert.ok(/1 package using directive/.test(output), output); +}); + +test("a using of a PARENT namespace passes", () => { + // `using A.B;` is legal wherever `A.B.C` is declared, so the index has to carry the ancestors. + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { "guide.md": "```csharp\nusing WallstopStudios.UnityHelpers.Core;\n```\n" } + }); + + assert.strictEqual(status, 0, output); +}); + +test("a namespace declared only under Generator~ passes", () => { + const { status, output } = run({ + sources: { + ...NAMESPACE_SOURCE, + "Generator~/Gen/Emitter.cs": + "namespace WallstopStudios.UnityHelpers.Proto.Generator\n{\n internal sealed class Emitter { }\n}\n" + }, + docs: { + "guide.md": "```csharp\nusing WallstopStudios.UnityHelpers.Proto.Generator;\n```\n" + } + }); + + assert.strictEqual(status, 0, output); +}); + +test("a using from another vendor is not this linter's business", () => { + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { "guide.md": "```csharp\nusing UnityEngine.Rendering.Universal;\n```\n" } + }); + + assert.strictEqual(status, 0, output); + assert.ok(/0 package using directive/.test(output), output); +}); + +test("an assembly reference that names a real asmdef passes", () => { + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { + "guide.md": '```xml\n\n```\n' + } + }); + + assert.strictEqual(status, 0, output); + assert.ok(/1 assembly reference/.test(output), output); +}); + +// -- Red halves --------------------------------------------------------------- +test("a using that names no declared namespace is reported", () => { + // The real defect this was written for: Core.Attribute for Core.Attributes. + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { "guide.md": "```csharp\nusing WallstopStudios.UnityHelpers.Core.Attribute;\n```\n" } + }); + + assert.strictEqual(status, 1, output); + assert.ok(/Core\.Attribute;/.test(output), output); + assert.ok(/guide\.md:2/.test(output), output); +}); + +test("an assembly reference that names no asmdef is reported", () => { + // The other real defect: the runtime assembly is WallstopStudios.UnityHelpers, and a link.xml + // naming .Runtime preserves nothing while reporting nothing. + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { + "guide.md": '```xml\n\n```\n' + } + }); + + assert.strictEqual(status, 1, output); + assert.ok(/WallstopStudios\.UnityHelpers\.Runtime/.test(output), output); +}); + +test("a nested documentation file is scanned", () => { + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { + "features/deep/guide.md": "```csharp\nusing WallstopStudios.UnityHelpers.Nowhere;\n```\n" + } + }); + + assert.strictEqual(status, 1, output); + assert.ok(/features\/deep\/guide\.md/.test(output), output); +}); + +test("every violation is reported, not just the first", () => { + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { + "a.md": "```csharp\nusing WallstopStudios.UnityHelpers.Nowhere;\n```\n", + "b.md": "```csharp\nusing WallstopStudios.UnityHelpers.AlsoNowhere;\n```\n" + } + }); + + assert.strictEqual(status, 1, output); + assert.ok(/2 documentation reference\(s\)/.test(output), output); +}); + +test("the repository itself passes", () => { + const result = spawnSync(process.execPath, [LINTER], { encoding: "utf8" }); + assert.strictEqual(result.status, 0, `${result.stdout || ""}${result.stderr || ""}`); +}); + +console.log(`\n${passed} passed, ${failed} failed`); +if (0 < failed) { + console.log(`Failed: ${failedTests.join(", ")}`); + process.exit(1); +} + +process.exit(0); diff --git a/scripts/tests/test-lint-doc-identifiers.js.meta b/scripts/tests/test-lint-doc-identifiers.js.meta new file mode 100644 index 000000000..3e30bac67 --- /dev/null +++ b/scripts/tests/test-lint-doc-identifiers.js.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3376611d4cd081722e8d7786d62b99e4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 21f0a34c71e34a79182c548e58245ec8bd9094d3 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 01:41:16 +0000 Subject: [PATCH 07/15] Fail a validation run that checked nothing Adversarial review of the headless runner added an hour earlier found it carrying the exact defect the code-fence gate was fixed for this morning: a run with no rules, or no assets, exited 0 and reported "validation passed" having measured nothing. Both shapes are reachable with nothing looking wrong at the call site. ValidationTargets.Enumerate skips a folder that does not exist rather than reporting it -- deliberately, because Unity logs a warning per missing folder -- so one typo in -validationFolder yields zero targets and a green build. A project that has not written a rule yet yields zero rules. CoverageProblems is a pure function of the two counts and the folder list, separated from Run so it can be asserted without an asset database. Both reasons are reported rather than the first, so fixing one does not hide the other, and the empty-assets message names the folders it was given -- the reader cannot otherwise tell "the project is empty" from "I typed the path wrong". Addresses #288 Co-Authored-By: Claude Opus 5 (1M context) --- .../Validation/Continuous/ValidationBatch.cs | 51 +++++++++++++++++++ .../Validation/ValidationReportingTests.cs | 43 ++++++++++++++++ .../features/editor-tools/asset-validation.md | 5 ++ 3 files changed, 99 insertions(+) diff --git a/Editor/Validation/Continuous/ValidationBatch.cs b/Editor/Validation/Continuous/ValidationBatch.cs index 856adbc21..e02120c10 100644 --- a/Editor/Validation/Continuous/ValidationBatch.cs +++ b/Editor/Validation/Continuous/ValidationBatch.cs @@ -88,6 +88,8 @@ public static Result Run(string[] commandLine) ); while (!run.Step(double.MaxValue)) { } + problems.AddRange(CoverageProblems(rules.Count, run.TotalCount, folders)); + string json = ValidationReport.ToJson(run, suppressions); if ( !string.IsNullOrEmpty(outputPath) && !TryWrite(outputPath, json, out string failure) @@ -155,6 +157,55 @@ public static List DiscoverRules(List problems) return rules; } + /// + /// Reports the ways a finished run proves nothing. + /// + /// How many rules the run was given. + /// How many assets it considered. + /// The folders it was restricted to, if any. + /// One line per reason; empty when the run actually measured something. + /// + /// A run that walked nothing, or that had nothing to walk with, is the absence of a + /// measurement rather than a pass -- and it exits 0 unless something says so. Both shapes + /// are reachable without anything looking wrong: a folder argument naming a renamed + /// directory yields no targets and is skipped silently by + /// , and a project that has not written a rule yet + /// yields no rules. Either way the build would report validation passing having checked + /// nothing, which is the shape #556 exists to refuse. + /// + /// Separated from so it can be asserted without an asset database. + /// + internal static List CoverageProblems( + int ruleCount, + int targetCount, + IReadOnlyList folders + ) + { + List problems = new List(); + if (ruleCount <= 0) + { + problems.Add( + "no IValidationRule implementation was found, so this run checked nothing. " + + "Write a rule, or drop this step until there is one." + ); + } + + if (targetCount <= 0) + { + problems.Add( + folders != null && 0 < folders.Count + ? "no assets were found under " + + string.Join(", ", folders) + + ", so this run checked nothing. Check the " + + FolderArgument + + " paths -- a folder that does not exist is skipped silently." + : "no assets were found in the project, so this run checked nothing." + ); + } + + return problems; + } + private static ValidationSuppressions ReadSuppressions(string path, List problems) { if (string.IsNullOrEmpty(path)) diff --git a/Tests/Editor/Validation/ValidationReportingTests.cs b/Tests/Editor/Validation/ValidationReportingTests.cs index 5d4f8ff8b..1226a3290 100644 --- a/Tests/Editor/Validation/ValidationReportingTests.cs +++ b/Tests/Editor/Validation/ValidationReportingTests.cs @@ -319,6 +319,49 @@ public void CommandLineValuesAreReadInOrderAndTolerateATrailingFlag() Assert.IsTrue(ValidationBatch.ValueOf(null, ValidationBatch.OutputArgument) == null); } + [Test] + public void ARunThatWalkedNothingIsNotAPass() + { + // The same shape this repository refuses everywhere else: a gate that checked nothing + // exits 0 unless something says so. A -validationFolder naming a renamed directory is + // skipped silently by ValidationTargets.Enumerate, so this is reachable with nothing + // looking wrong at the call site. + CollectionAssert.IsEmpty( + ValidationBatch.CoverageProblems(2, 17, null), + "a run with rules and assets measured something" + ); + + Assert.AreEqual( + 1, + ValidationBatch.CoverageProblems(2, 0, null).Count, + "no assets is a run that proved nothing" + ); + Assert.AreEqual( + 1, + ValidationBatch.CoverageProblems(0, 17, null).Count, + "no rules is a run that proved nothing" + ); + Assert.AreEqual( + 2, + ValidationBatch.CoverageProblems(0, 0, null).Count, + "and both are reported, so fixing one does not hide the other" + ); + } + + [Test] + public void AnEmptyRunNamesTheFoldersItWasGiven() + { + // Without the folders in the message the reader cannot tell "the project is empty" + // from "I typed the path wrong", which is the only actionable difference. + string problem = ValidationBatch + .CoverageProblems(1, 0, new List { "Assets/Typo", "Assets/Audio" }) + .Find(entry => entry.Contains("no assets")); + + StringAssert.Contains("Assets/Typo", problem); + StringAssert.Contains("Assets/Audio", problem); + StringAssert.Contains(ValidationBatch.FolderArgument, problem); + } + [Test] public void EveryConstructibleRuleIsFoundInAStableOrder() { diff --git a/docs/features/editor-tools/asset-validation.md b/docs/features/editor-tools/asset-validation.md index 81114e464..6e3913af5 100644 --- a/docs/features/editor-tools/asset-validation.md +++ b/docs/features/editor-tools/asset-validation.md @@ -163,6 +163,11 @@ skipped — one broken rule must not hide every other rule's findings — and th which is not the same as answering "nothing wrong", so passing on it would report coverage the run does not have. +**A run that checked nothing fails too**, and says which half was empty. No rules, or no assets, is +the absence of a measurement rather than a pass -- and a `-validationFolder` naming a renamed +directory is skipped silently, so a green run over nothing is reachable with nothing looking wrong +at the call site. + The report carries a `schemaVersion`, the counts, every finding (suppressed ones included and marked), every failure, and any suppression entry that matched nothing. From 5404782bb2ecfdcf3e5d64632b19f3f46bff767e Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 01:53:05 +0000 Subject: [PATCH 08/15] Bind a reservation to subtype numbers, and lift one retirement at a time Two real defects Cursor Bugbot found in the first draft, both silent data corruption. Restore dropped sibling retired numbers. `restoredRetirements` was keyed by subtype/base pair, and the final loop then dropped EVERY retirement for that pair. A pair can hold more than one -- a hand-edited number leaves one and a later deletion leaves another -- so re-adding the type under the first freed the second, which is the exact reuse the record exists to forbid. It is keyed by pair AND number now. Verified against a mutant: three new cases and two existing ones go red when the key is coarsened back. Reserved numbers skipped subtype tags. WPROTO043 inspected only [WProtoMember], while a base's includes are numbered against its members -- one space -- so a rule binding half of it was one an author steps around by writing the number on the other half. [WProtoInclude] and [WProtoSubtype] are both refused now, at their own diagnostics so each names what the author actually wrote. That has a second half Bugbot named: the assignment tool has to assign around reserved numbers as well, or it hands out a number the next compile rejects -- a deadlock, which is the thing that tool exists to remove. AddReserved now feeds [WProtoReserved] numbers in beside the members and includes. Bugbot's third finding, a validation run over an empty folder exiting 0, was already fixed in 21f0a34c. 707 generator tests against protobuf-net 3.2.56, 706 against 2.4.9. Addresses #606, #608 Co-Authored-By: Claude Opus 5 (1M context) --- Editor/Tools/WProtoSubtypeTagAssigner.cs | 22 +++++ Editor/Tools/WProtoSubtypeTagPlan.cs | 12 ++- .../DiagnosticTests.cs | 46 ++++++++++ .../SubtypeTagManifestTests.cs | 81 ++++++++++++++++++ .../ReservedMap.cs | 24 ++++++ .../WProtoGenerator.cs | 22 +++++ ...opStudios.UnityHelpers.Proto.Generator.dll | Bin 199680 -> 200704 bytes docs/features/serialization/serialization.md | 11 ++- 8 files changed, 212 insertions(+), 6 deletions(-) diff --git a/Editor/Tools/WProtoSubtypeTagAssigner.cs b/Editor/Tools/WProtoSubtypeTagAssigner.cs index e65fc4724..8b888f56b 100644 --- a/Editor/Tools/WProtoSubtypeTagAssigner.cs +++ b/Editor/Tools/WProtoSubtypeTagAssigner.cs @@ -312,6 +312,28 @@ WProtoIncludeAttribute include in baseType.GetCustomAttributes( + false + ) + ) + { + foreach (int fieldNumber in held.FieldNumbers) + { + inventory.Reserved.Add( + new WProtoSubtypeTagPlan.Entry( + "[WProtoReserved]", + baseName, + fieldNumber + ) + ); + } + } + const BindingFlags Declared = BindingFlags.Public | BindingFlags.NonPublic diff --git a/Editor/Tools/WProtoSubtypeTagPlan.cs b/Editor/Tools/WProtoSubtypeTagPlan.cs index 049550c0f..bf8ed7563 100644 --- a/Editor/Tools/WProtoSubtypeTagPlan.cs +++ b/Editor/Tools/WProtoSubtypeTagPlan.cs @@ -205,7 +205,11 @@ WProtoSubtypeTagDiscovery discovery StringComparer.Ordinal ); HashSet keptPairs = new HashSet(StringComparer.Ordinal); - HashSet restoredPairs = new HashSet(StringComparer.Ordinal); + // Keyed by pair AND number, not by pair. A pair can hold more than one retirement -- a + // hand-edited number leaves one and a later deletion leaves another -- and re-adding + // the type under the first would otherwise free the second, which is the exact reuse + // the record exists to forbid. + HashSet restoredRetirements = new HashSet(StringComparer.Ordinal); foreach (Entry entry in Safe(existing)) { @@ -295,7 +299,7 @@ WProtoSubtypeTagDiscovery discovery && wasRetired.Tag == declaration.Tag ) { - restoredPairs.Add(key); + restoredRetirements.Add(RetirementKey(wasRetired)); } } @@ -310,7 +314,7 @@ WProtoSubtypeTagDiscovery discovery // Remove-then-re-add, which is the case the whole design exists for: the number the // type had is still held for it, so it comes back rather than being handed out. assignments.Add(entry); - restoredPairs.Add(key); + restoredRetirements.Add(RetirementKey(entry)); keptPairs.Add(key); } @@ -340,7 +344,7 @@ WProtoSubtypeTagDiscovery discovery foreach (Entry entry in allRetired.Values) { - if (!restoredPairs.Contains(PairKey(entry.SubTypeName, entry.BaseTypeName))) + if (!restoredRetirements.Contains(RetirementKey(entry))) { retirements[RetirementKey(entry)] = entry; } diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs index d55d11d77..83216d04f 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs @@ -1146,6 +1146,52 @@ public void ARemovedMemberComingBackUnchangedIsAllowedOnceItsReservationGoes() ); } + /// + /// A reservation binds subtype discriminators, not only members. + /// + /// + /// Reported by Cursor Bugbot against the first draft, which checked only + /// [WProtoMember]. A base's includes are numbered against its members -- one space -- + /// so a rule binding one half is one an author steps around by writing the number on the + /// other. + /// + [Test] + public void AnIncludeCannotTakeAReservedFieldNumber() + { + AssertDiagnostic( + "WPROTO013", + "is reserved on 'Base'", + @"[WProtoContract] [WProtoReserved(100)] [WProtoInclude(100, typeof(Sub))] public partial class Base { [WProtoMember(1)] public int A; } + [WProtoContract] public partial class Sub : Base { [WProtoMember(1)] public int B; }" + ); + } + + [Test] + public void ASubtypeDeclarationCannotTakeAReservedFieldNumber() + { + AssertDiagnostic( + "WPROTO040", + "is reserved on 'Base'", + @"[WProtoContract] [WProtoReserved(100)] public partial class Base { [WProtoMember(1)] public int A; } + [WProtoContract] [WProtoSubtype(typeof(Base), 100)] public partial class Sub : Base { [WProtoMember(1)] public int B; }" + ); + } + + [Test] + public void AReservationOnABaseDoesNotRefuseAnUnreservedDiscriminator() + { + // The refusal is the reserved set exactly. One that swallowed the numbers beside it + // would push every later subtype up the number line for no reason. + CollectionAssert.IsEmpty( + Run( + @"[WProtoContract] [WProtoReserved(100)] [WProtoInclude(101, typeof(Sub))] public partial class Base { [WProtoMember(1)] public int A; } + [WProtoContract] public partial class Sub : Base { [WProtoMember(1)] public int B; }" + ) + .Select(diagnostic => diagnostic.Id + " " + diagnostic.GetMessage()) + .ToArray() + ); + } + [Test] public void ReservationsDoNotChangeWhatTwoLiveMembersOnOneNumberReport() { diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs index 838518768..9542edf10 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/SubtypeTagManifestTests.cs @@ -478,6 +478,29 @@ public void AFreshNumberAvoidsTheBasesOwnMembersAndItsIncludes() ); } + [Test] + public void AFreshNumberAvoidsWhatTheBaseReservedWithWProtoReserved() + { + // The other half of Bugbot's second finding. A reserved number is spent as surely as a + // live one -- the generator refuses a discriminator that takes it -- so assigning + // around only the live numbers hands out a number the next compile rejects, which is + // the deadlock this tool exists to remove. The assigner feeds [WProtoReserved] numbers + // in through `reserved`, exactly as it does members and includes. + WProtoSubtypeTagPlan plan = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base") }, + new[] + { + Entry("Id", "N.Base", 1), + Entry("[WProtoReserved]", "N.Base", 2), + Entry("[WProtoReserved]", "N.Base", 3), + }, + NoEntries, + NoEntries + ); + + CollectionAssert.AreEqual(new[] { "N.Sub=4" }, Describe(plan.Assigned)); + } + [Test] public void AFreshNumberSkipsTheReservedProtobufRange() { @@ -702,6 +725,64 @@ public void ReAddingAnExplicitlyNumberedSubtypeTakesBackTheNumberItHeld() Assert.IsEmpty(restored.Retired, "the number is in use again by the type that held it"); } + [Test] + public void ReAddingATypeLiftsOnlyTheRetirementItReclaims() + { + // Reported by Cursor Bugbot against the first draft, which keyed the lift by + // subtype/base pair. A pair can hold MORE than one retirement -- a hand-edited number + // leaves one and a later deletion leaves another -- and re-adding the type under the + // first freed the second, which is the exact reuse the record exists to forbid. + WProtoSubtypeTagPlan plan = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base", 5) }, + NoEntries, + NoEntries, + new[] { Entry("N.Sub", "N.Base", 5), Entry("N.Sub", "N.Base", 7) } + ); + + CollectionAssert.AreEqual(new[] { "N.Sub=5" }, Describe(plan.Assigned)); + CollectionAssert.AreEqual( + new[] { "N.Sub=7" }, + Describe(plan.Retired), + "7 belonged to an earlier version of this type and is still spent" + ); + } + + [Test] + public void ANumberAPairRetiredTwiceOverIsNeverHandedToTheNextSubtype() + { + // The consequence, driven one step further: with the retirement dropped, the next + // tag-less subtype was handed the freed number. + WProtoSubtypeTagPlan plan = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base", 1), Declare("N.Later", "N.Base") }, + NoEntries, + NoEntries, + new[] { Entry("N.Sub", "N.Base", 1), Entry("N.Sub", "N.Base", 2) } + ); + + CollectionAssert.DoesNotContain( + plan.Assigned.Select(entry => entry.Tag).ToArray(), + 2, + "2 is retired and may never be handed out again" + ); + CollectionAssert.Contains(Describe(plan.Retired), "N.Sub=2"); + } + + [Test] + public void ATaglessReAddLiftsOnlyTheRetirementItReclaims() + { + // Same rule through the tag-less path, which restores from the manifest rather than + // from the attribute. + WProtoSubtypeTagPlan plan = WProtoSubtypeTagPlan.Create( + new[] { Declare("N.Sub", "N.Base") }, + NoEntries, + NoEntries, + new[] { Entry("N.Sub", "N.Base", 3), Entry("N.Sub", "N.Base", 8) } + ); + + CollectionAssert.AreEqual(new[] { "N.Sub=3" }, Describe(plan.Assigned)); + CollectionAssert.AreEqual(new[] { "N.Sub=8" }, Describe(plan.Retired)); + } + [Test] public void DemotingASubtypeToTheManifestKeepsTheNumberItWroteByHand() { diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs index f96847571..03beaf5d1 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/ReservedMap.cs @@ -121,6 +121,30 @@ internal bool ReservesNumber(int fieldNumber) return _numbers.Contains(fieldNumber); } + /// + /// Explains why a reserved field number cannot be taken by a subtype declaration. + /// + /// The number being claimed. + /// The contract that reserves it. + /// The clause an include or subtype diagnostic appends. + /// + /// Members and subtype discriminators share ONE field-number space -- a base's includes are + /// numbered against its members -- so a rule that bound only [WProtoMember] would be + /// one an author steps around by writing the number on an include instead. + /// + internal static string ReservedProblem(int fieldNumber, string contractName) + { + return "field number " + + fieldNumber + + " is reserved on '" + + contractName + + "' with [WProtoReserved]. A subtype's number and a member's number are the same " + + "space, so a reservation binds both. Every payload written before the removal " + + "still carries that field, and a discriminator sharing it reads those saves back " + + "as the wrong type. Use a free number, or delete the matching [WProtoReserved] if " + + "this really is the removed declaration coming back"; + } + /// Whether a member name may not be used. /// The name a member is declared under. /// true when the contract reserves it. diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs index f6b6cd72a..dd95780dd 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs @@ -2903,6 +2903,7 @@ SubtypeMap subtypes { List includes = new List(); HashSet claimed = new HashSet(); + ReservedMap reserved = ReservedMap.Build(contract); foreach (Member member in members) { claimed.Add(member.Tag); @@ -2966,6 +2967,12 @@ SubtypeMap subtypes // accident (#606). problem = SubtypeMap.RetiredProblem(tag, contract, retiredBy); } + else if (reserved.ReservesNumber(tag)) + { + // Checked before claimed.Add so a refused include does not spend the number it + // was refused for. + problem = ReservedMap.ReservedProblem(tag, contract.Name); + } else if (!claimed.Add(tag)) { problem = @@ -3010,6 +3017,21 @@ SubtypeMap subtypes continue; } + if (reserved.ReservesNumber(declared.Tag)) + { + context.ReportDiagnostic( + Diagnostic.Create( + WProtoDiagnostics.BadSubtype, + declared.SubType.Locations.FirstOrDefault(), + declared.SubType.Name, + SubtypeMap.Written(contract, declared.Tag, declared.TagFromManifest), + ReservedMap.ReservedProblem(declared.Tag, contract.Name) + ) + ); + failed = true; + continue; + } + if (!claimed.Add(declared.Tag)) { context.ReportDiagnostic( diff --git a/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll b/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll index 228729599ad43901f80b79a5fe5f9d9cc25efd5b..126515ca3d43a46ef3f0d8bab39c3f83e6419717 100644 GIT binary patch delta 38929 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 delta 38505 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 Date: Sun, 30 Aug 2026 01:59:30 +0000 Subject: [PATCH 09/15] Reserve the name a consumer sees, not the C# identifier Same shape as the reservation-skips-subtypes defect a review just found: the rule read one half of the space. [WProtoMember(9, Name = "Health")] presents itself as Health to a generated schema, a payload dump and anything matching by name, whatever the C# member is called -- so a reservation that inspected only the identifier was one an author steps around by renaming. Both names are checked now, and the diagnostic names whichever one was taken. The other direction stays legal, because it is the point of the override: a C# member called Health that presents itself as something else takes nothing back. The schema exporter already renders the override, so the two halves now agree -- a project that compiles can no longer produce a message carrying `reserved "Health";` beside `int32 Health = 9;`. 709 generator tests against protobuf-net 3.2.56. Addresses #608 Co-Authored-By: Claude Opus 5 (1M context) --- .../DiagnosticTests.cs | 34 ++++++++++++++ .../WProtoGenerator.cs | 43 ++++++++++++++++-- ...opStudios.UnityHelpers.Proto.Generator.dll | Bin 200704 -> 200704 bytes docs/features/serialization/serialization.md | 4 +- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs index 83216d04f..601ba34ce 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs @@ -1067,6 +1067,40 @@ public void AMemberCannotTakeAReservedName() ); } + [Test] + public void AMemberCannotRenameItselfOntoAReservedName() + { + // [WProtoMember(Name = ...)] is what a generated schema, a payload dump and anything + // matching by name actually see, so a rule reading only the C# name is one an author + // steps around by renaming. + AssertDiagnostic( + "WPROTO043", + "the name 'Health'", + @"[WProtoContract] [WProtoReserved(""Health"")] public sealed partial class Save + { + [WProtoMember(9, Name = ""Health"")] public int Hp; + }" + ); + } + + [Test] + public void RenamingAwayFromAReservedNameIsAllowed() + { + // The other direction, so the rule is about the name that reaches a consumer rather + // than about the identifier: a C# member called Health presenting itself as something + // else is exactly what the escape hatch is for. + CollectionAssert.IsEmpty( + Run( + @"[WProtoContract] [WProtoReserved(""Health"")] public sealed partial class Save + { + [WProtoMember(9, Name = ""Vitality"")] public int Vitality; + }" + ) + .Select(diagnostic => diagnostic.Id + " " + diagnostic.GetMessage()) + .ToArray() + ); + } + [Test] public void AMemberTakingBothAReservedNumberAndNameIsNamedForBoth() { diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs index dd95780dd..848237053 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs @@ -3219,8 +3219,18 @@ NestedCollections nested // Checked after the duplicate, because a member colliding with a LIVE sibling has a // fix the author can see in front of them; a collision with something deleted needs // the reservation explained. - if (reserved.ReservesNumber(tag) || reserved.ReservesName(symbol.Name)) - { + // + // Both names, because [WProtoMember(9, Name = "Health")] presents itself downstream + // as Health whatever the C# member is called -- and a rule that read only one of + // them is one an author steps around by renaming. + string schemaName = SchemaNameOf(attribute) ?? symbol.Name; + bool reservedName = + reserved.ReservesName(symbol.Name) || reserved.ReservesName(schemaName); + if (reserved.ReservesNumber(tag) || reservedName) + { + string takenName = reserved.ReservesName(symbol.Name) + ? symbol.Name + : schemaName; Report( context, WProtoDiagnostics.ReservedTag, @@ -3228,10 +3238,10 @@ NestedCollections nested contract.Name, symbol.Name, reserved.ReservesNumber(tag) - ? reserved.ReservesName(symbol.Name) - ? "field number " + tag + " and the name '" + symbol.Name + "'" + ? reservedName + ? "field number " + tag + " and the name '" + takenName + "'" : "field number " + tag - : "the name '" + symbol.Name + "'" + : "the name '" + takenName + "'" ); failed = true; continue; @@ -3401,6 +3411,29 @@ params object[] arguments /// cannot reference the runtime assembly, but it can read the symbol the argument is typed /// as, which is the same declaration. /// + /// + /// The schema name a member declared for itself, or null when it declared none. + /// + /// The member's [WProtoMember]. + /// The declared name, or null. + /// + /// Never written to the wire -- protobuf identifies fields by number -- but it is what a + /// generated schema, a payload dump and anything matching by name see, which is exactly + /// what a reserved name protects. + /// + private static string SchemaNameOf(AttributeData attribute) + { + foreach (KeyValuePair argument in attribute.NamedArguments) + { + if (argument.Key == "Name" && argument.Value.Value is string declared) + { + return string.IsNullOrEmpty(declared) ? null : declared; + } + } + + return null; + } + private static bool AsksForZigZag(AttributeData attribute) { foreach (KeyValuePair argument in attribute.NamedArguments) diff --git a/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll b/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll index 126515ca3d43a46ef3f0d8bab39c3f83e6419717..4da2208d305a5b996f62955db2bcafb7bbca5b31 100644 GIT binary patch delta 30791 zcmb7N31Cyz(w=i}nxborZ2nxLu_gp&Xj$!faHR|kq5IXOt3 zDP|<}Mj`!l!T(7J0z-m;3tZ{=eVu|{Fc#4Jk*y?HlLRV$SQCD?hM2uPx5nM?c zdlsN4v+j~vex)+wrAz!?#!I@1Jh0295hp>v zkmf2kHWwhtX#`cM?W0>)P{i`;rg<$d!M1O9>&9c$@(fMu&#kRRH&-`C$MhvJtv~$5 z8MQrzQ|wzElvLY~0tA#-+wGcwXFp&Wm9Nv}kE#h)myP^Ni$O8iJ7cS01jSHqv}UGo z9v&)XEyAkocz39P39GUb$fEIa>qsG%HyUEz62U5VRO;rbdt>{w(AxKr#62(sCe+QR zru_lThfLbX6{kIf5^1kPNrA{!+;c-=g0$x&buYqZ-2!!C+|}YC^*~%rFn6xIPfd+) z8`0d+&GBRW+5_=D6Lm`pX0XqrX#SX038lHD)?((zDtfdV!(f7aFb$A9yC)#8o3B2e z(8=WUrtZ7lb|u&U+#ahs6H{G2`%9x!Lb_*a`je_ptoG;sZsOQ01?irYbkX1^CtZkJ zi|*=hprHjeW3;pr#b4{xImvyS>w+|05bDk*-!GoVBi1F>s?Vh)Td0$77sYXJJTF^E z)3Vb%*5P3tnqL*j$c#dhrZ`(?WT4txFJr6M5i1f45Fhx3@^Y zY(JYiq)l+6IkiTL^^LvLZwNATnT_^=`eNGBa%_u$%3p51)#OxVU&*A&*%LVh{t5X! zCnf7jz5KJ7XfM{1s-qQej2jTD_HeaVm)Mj2zFxI=6Z6%Y9GjZlDk*dxn&n#YSh!8y zn3L`|8r|v|G}_c^EMi)29%A?0F+r3%Z`-|IILe<>zT=jpfSk<17wzch9GT5Kf8{7h3Fsj0edss|*x;(& zX~(F2oTZ3!oVkcEI2{*lt2562{uHip4e__1b+s4s8-H-M5TQ*$)C*%W)Yiq>7x@@n zya;{&T|B|xdrArRRyUPoTx5By=Cf2bFYzg_oFxI zK9J0(@&3w2u|S>MzOBEWyV|GqU5F-lIUz&EzFIpDG_od;Ji&zgDY`#w;&s{TGyq=I z_(ySPr*foHTU)2q1WvzWZvA7elF5W3g-#0n?sbF4mC6?YAHcke(8X@3;0-BYo7 z@rLlZo5Sv&O{3tHVRLt9s}gb5gjXl}$6ZjJSE^k)wRGlkQd9%Qy*1tMMZ5G?XEX`a z`PIqsfg`&$P`SN2#=3vP5 zjm-TFH`Qe|ZBn$}|5D7xvo#M%F|ToU$5{dwlc`hFV(Qu{t-RS{+Ul<=uB?{>$!kmk z7n8cGQzcH)kxq#vf#vlt9BqXv?!0VY%G!zO?#}|XuyedVFM(dQdaE~fPVrp2E;J>g zwBwTiE}APYO0>)6#eNlC((%^Zx7f*zzD}P*HA25yS~dTBei>%czswG-y>dXeQ8xV@3-t1mGepq_cA_mnyPo@Js8^6mD4n_d~O86 z2>-cpXRD?>)SJ4tbzWh*@R}b4$1a)$bx+r(?fpa7H1VKnz9zOmxrq(UOI6%o+wiJt z-%K-f8kpWk9-3Pg;4(e7GeQA}|)!p(OO%o**ce?_t)*CdQfP3#|iK~Bg z^X)HfA+|5{1>;Tn-*T&TTx9MZjS?+pvp`pX$qTapr+jsd{?EHM>Oqzpe-K)DL@PvbERI>Yq5T-7+ylr)HA(?`7f#0_3oUZg_y}o zj2vq<+!wUg*^$m$er+GGNVemmdV^Enti=l(Y`P&~28i0ZG2KVqw zJcC>L+s_T2oX+|B5=Ga>+-nqk5g-*_3KD&H9MTzm%p21DIq1XDS!%Z%lTmZajgtbN zx^VW-sE6ZY)qieu7P0foK6lU{q|yEv)?8b2z6XzCLz@=FuA#{RH=~z4y8JV=$Ul@e zwSC+FKcWQ;*mowWyK3iLDcYD}$<3c~9vasFO7`NWrYG3ao2FdJ6kTg~9+yk&x0{*@ zQagOGe&7aPuGJ5Q$BTLDx5K;p%h7g3ufYN3xXiOSPC#CT?{&{1TKe?5r|FuexF2sv zy8<6ZjP5=a%~ZH3PK+26c>R)gb`kK1M-%L};43GS`_1N#^-b4wo1l%l+1ROL(0}P@ z@oj?;|28;0vhNi)jJ_{1|LbpF)BF*2KaDdWe>HxT{i5yIV^nWk?vIahA$~ZjQ{dBQ z`j}Q~*IQ^e+%~Kv9<}AGAZrv zV@lG#2hn~2LE9%>CfsCaQPTAO>hzdV=y7y$iaK%Z8`yGv;QB7wy;E z`(9}i{&Rb2;M)23H!teIJ39N*e(H`XftQlY^pbWd)X3XgG#^>x=&6fx#h2>etLKW{ z>YO#T!lRyClOY-!!=BhF#JlSLC)#6u0lGH82scVU% z_mJdAS;F;ZT|LV4rXky@PIxNAK(=bsT3h4OPkkPQL1sVmPNavrlvvE}7QteI-P1Vm z*;FAsjZ>fNE9|5z$T7L^Gm(l&qRHJY#781b?&we=>iOsUi(~3F8{39XBddZ)nDMy! z;l@05>&6IiXXCpYlY}^~9(&;#@u|A7@kKFJ?YyajIH=ygNp;*wJ;E%ZmT zTJgy%-NQH~>?$#r`?gHxle_tA5?0PrW4Bg|L+bTgug9hS+}4|FX?J5MLiheieUQ>U zI?C77UZI#n?AMcx!ECS()LaoNh9I-S9AqDLspu9>q)vP}Bb)~1e_gZuWjntGk?spk zL9_%0Tf_v#-3!mzrZeA{od3bS`LI;WE3qD`sB}Msy@Ts2@C~u zpDr>8%n+U7<7^UxzcK@tt?GC! z1M%9|&WWjwSHFIP#A>hD7E|>v)&4D}dA0TTO&qD&)Sln=3cp8aUIVhz^U_{aamT>w zs>ilFGqvCsjkI}3^~l>Xd&I<~jeZL~U#9^QyU42fVdb7i$|v zbD~DSoq>Hy-==F>!`m&I@3-}Dm!KFYwsq6$GY8dufP>9XmhJ{TX>{ryl+F+ny z{CDEj3-5%fz22$PqX*VOUGYv%8N24!MVFZ%DyxDg#|-?m(vV#c*K$TUHvTpqR{whE z>UJMCPlV_tiTvBCj1?XoMmI|a>)!(ctqC~KY)?T!c)I8h(rul!77L7LfEfYbM!iKmaG7gr7MD4*X| zu3rkY_2Pf$OIt;}*T%nXs^9D8Z{rry&WbB@ZhoOSjp9PK!pn0&@iFuhik^MWOj!FPd7c zRp0;3fj)J%zZK)q{EYuLaWC$9S3id-xNqA3TQ|*gKmMYftuGCt7cUL>wdua~>He3! zCDcFPf51aGK%vH_+aIHT_k&`i`&OS5Bi}$6-TVClcSD9Az^6Ov7q}xceLA zbBV86WOUEV(v6jZFr$01j~U$o_EFhBhq$cU?xhbxhwW`qNIM*J=UXXi@=LdW6{yeb zwf{fIo7d78*Zq#asn^1uCP>p!#Jn-f4GF?3jF$+gk1Pc3cP2ej}k*+t-9}Ep&0YfAzM;Cj>Q^m2`=xh z;KwaS{dot4ZQH4iL+v_RE!OfsC914-g?JBYHDTi^JbUouu!dNI>+mgJsMQv2vwuqm zQeJ>0)_#(7G0`#V%0)Ky%|io3FE#0_&clPrU9kN&43b@7YH2a@jfpkFq_^t+#Czg` zP!m3!P%BTwSt`sLxY5?`MJtGf@D$`b2N z@2}rcRiu9RRc21?s_aCzU1cAFLs(^>f~m$uw28rwLoo5% zsbrCsNVU!3u2)5Aw^e#@OPIxE33kt+nYjbau>F|79cc)Pj<(;8qp(^mmV(~W5?v5) ziBfkQzQ$&;nCu>mRaZ+eiAHKs(~l(OSnBZWccIY+`+8Kgz3;+5|AZ7sW3CNv_JZL? zOsEcXn)I1^^O39K`Jpw$y%i%`jd>j`LHZ-Ay6s4;PZ?ao${1AYPpM~)j0ib^xfW#0 z8LQQ_qj9SHs5=4gD_{b;mUa6ua9Fgks@+pzcl(B?&(d|U_yQ4W`lxq39t+-3={`CamR$u#i zxR|7-9~&hmtM?weUK~}A9;*^l)b!)7&i7-=6u*_&259$OnD_l&$G|Ypq)oPnhu~_NKKsoH(yV^iEq? z@|RNa`-v8rZ<2NSGzJ-)E8U&cdEaD4AYU3lgpU0rkK1bd4ZEJBt6(_C9)5uE!h2y{l z?W9(aV(C7tI#0%nuhp(6^TizXwv#Vgr=ziw8d~Q@FR>02zLs00cK9ycx(F)~95)~= z*!aNcqEor|Nc+R;_U~fUdEbQ#Y9&QrC;S9Ev}Nwgny!%L8EW+RR=w5b`$6ckc82=&`Ee+o_SC0Q&G~7F zXi)F|DJO2T-Um-F!(D&)`n;y@_^GRy*%)y)Sl|rBoXd%OLpQ{4p&IVO*K?Zlx7EJq zx{BHAW9QQF#n{O5_?ewftfSZ!; zz6(O7eZM;Q=MElzvZBc{*!QA>W}KgKG|@g?#P{)<=wn^PH*T8f6Bc2uZomo2Eu8cP z^7?ShgzCq##7Xa?j5k{hZZ`htzV22$#?))hcTv=~LOpvvRo!$xUEH94dcLh#q*{I{ zja}LfHw_&oxt$dL9#28AVxOfB{$+?bslNP6A#RWFet9|iB)XQiyOJq0^0XDP~klDX448{`UB^WY0_wU*fe~&}{p!&%l#Wbt1 z8n1;JD4m7vtMT`T_`iGA^gmsm5ImUZ#|YcvQVWdmTs&cU?4$XSFuj1}pTurWGQuPA z?`Ig{hB(4F#uCPsEFT(6@=s!@?~eG-QjM^nh%h~t@a~e&QbXaV7FCW=*pWiGHiFQV zUgg1G`%qzqxc28NjW96HmJ|vHGD&?5(F`}CJQ_O6sK`+8q?3GjYL%l7j^GJphQ8_3 zvdr*B+O(`t$V0giN@P1F6z=0-*RsT!MoGLVD2dk74Dz^qTM)C#K zNvCojMi$PjM=7yjo$W{<7S1dRlDJa?Vz;gwp>kf;#-{B06 zEg|NCRZgOP;7L}-RTCQo&oE2ofTLjtv-6pxyA6J3)`fLwAr;sc?iM?q^25iUMw-7TCceKq0K9!(^MNs4!$5Y~vn z1?kW>bO2(Nsh{_(5OG~=v0GI4krXne+KmQ}CAhs)q-bTOK84YQza$ZUlT_>7h<1Ij1JZY`w-xjY z2qR#Ik<3N}&kPpuA!B(+ZGnJIC>PKwm~acP%EIJkZ>KhMQ9%!_l89BunMtH{VVNgl#OXT zO7?crKy9^(a5bam^UmOz-bO`OJmhYAhzC1s^YMEanf@9@mB~$8>Q6#*bdX&<$4+-~ zJB|ysB!(&x96@-WLL>euW@fMpI%YkNl5wt8hz*5LBWj*IMNpK-5@r_Sulbzssvr`r zGgIe-*)$HCQ3c+x5H7DEJnx-oz``l%L>^|*b4ApC8yA8C7cjm8C(s>g6R91^l4xEa zi()CGkD^|{ES|N!f_O{lZVa{)<$del;(VcUH_ERK+Kad?=rgR!i>UQ2H*Fm(@T4Ct zD2AyiClKc*68;lS7;Pr(TSEA2GGQZ2UM^;dh0u^sSjje9BB;H+fbda9FI$eSAW1I+ zVJG&pswK5Au=bV|50Q7-brGAI+1)qoNS@4+u{LTy$RUq$w?A0hBa7q@NMvR@3t~E?g_8g7IE-|-+vK5N--l$PdbNp;gPn}08Lc5RgUj$mdqH6= zd<#X0!$oVvET~4D0bLP;1mR=Q7jYX5M$8bS3XKqNye(kFR26N+58^sC!u^Pp)J4!9@2JkN+%v;oant)H#Ho_F}! z6UgTH8R-_M;Uxw@g(UQ8keYw0&?9+yF4^Gy(zZEL$iTG>uzH_tk9V)eYY_Y;#;yec)fJDF&QQ?Q(PXd_>nH|0GZji=9`p_i6*-=u6jii?*_x5*KZ(RP=xkg8 zv3)vQmQL)v&TcXgn_fcBcgUf(rH*#6UuO$qmO3h+BYsv$x;c)ej!HPFv-9yQ9qnOU znWj6_uW+TK8rI_dC*}w#$txXK!R$5~OGsGh=m>X|6PtzkuXA*QS$_7sqYG@*n5i}~ zRdj=`I@{|V6o!*eBkKmg>%z?~iG{Ug*XV4EqZ=eLQ(?LJZAW*=*Vz}LyB$5Cq8(Xk zv(^(TDv8a4NzRWPz2P8!zk=q_T71gU7rNo?GO<&xpB?>SFf%N-Cr>!9gAX-oI*cDn zj)3FLsQNL^5l~;Fb=;hpm zIwwO~CylwYIytApTg(>1;glZEJK?yli%DGSm<|=4HOoH|+B)xo*O@JZww{Ro&Y5t5 zskR{Q2JK4)JLDN$0r$Yp?pnv+l_Ac#aI%NS?l3HMEP(SpHI|pX((xcbZ;jO@tKuQ} zirEg?fR+!zNuALmQ{m*bn&nNUp0Un{;o81TaqcHLAAwt#sqjVUZ0BN_jqh80v%Lfs zT}Mot?MGqF0AdSq2cB{~4kZIMmR)k9a0SfL*(ulk&Xu4tn~f2cI9I_sX0vEBvgoWbbGh?LNYvSp z&RXwDv4_u7^ihINOwki(vz7 zVWvXsm^ID~lmq_k2&Qd~|1bdm>(`=IN$O!Ys=jM?CEe-yzSEkRhBMwybYgmC>0Jw?Q^~ZzcZuL~ywKE#HSl zI-^>C0Ke(Xh}GK*zYHZy%H@#rLrAY>W;&2^-1!l-(%EW9s`v<8%oV85os5a9BzwTPBn=tJ=AaO(6;2#wN)~jb#|R2)O8g0FvFsF zuqt1}wvlAo092J@5H(6;BeRw|jzbrnJ)N@DaRRpJj4JjGtR1acUMO7Z_!icT(b)aC zq)tNGIE{UrywveM^d7IVwk?)APQmB5YOK&Ue5vC!81UmR43AZbbo~I+nb9Gg?)n|< z6G*4R(^*azh{ennLU>x0%P5ZM?5B)wt`PCyM6z56U*RoGm^i7kI75-wVHF7}-Ef1O z#j9>IYB05Bc3fxi-jP<(DrIaEne?BenOvKc0cJ5oXYYB}TSZbkx zr8}#iD^8S7BeoDeN!aX65M6XemzN}Q3p4GSks`)TCrj;`(Lxv+h&4c8^qD43>1+hD zbYYsQ>DFZqc4dgcvo-cZSm&|MZ1Eb?h45@zv1}rT}Dsk0u1 zpM^VwZ9ezIOVBLTIUgW43l?YJ=W>en%v5+bb%DzzChEGoqaJn@i+hZ%lr7m~R)*&T)bA&t?Fy;`i*8C?@P;m0tVj*8YGcp3yu@ai9x`V@FYy{P723Bb_Q+o1Lrn?iQpP&_h-7@ZNX7akWvA;} zagvz|e@A@a>L=DLAsw=uPhHoGtmPV0s!tUT76X{!9*sKY8Y0{)G#%YH4;6RmY-!S| z!dh{k&erFhD)bB!iGh}zX!zvyI9G)An@*%Mi&qOroZM$=7`f4IhqEzGpXvGE@9DN~x&h@$c0 zG_zTz4HfA{b>ciTx|3;%Ec{7wGYdywQZ!NEjRO9t@Lb`ELbs^bSzb(a(IheVDNQ#c z?b@O{#2#h~A<}R|(KL~TUkspr%Tp&5%@Uq^rnp0`CkpQoYjyU9^^T%B;)u>VCe|0- zD_k3><3e7H_lrtql=gi^_lv&FXrC-AnlA=x4AWkTEdLqmHw#`2T3hsh=&G|d2^)(R ziU}U3+P(N95&i;)fY6#(iXIUk=5o8EC=$)80}h~yVF9X;GV zC1!8d0@BUlIuSU-WuaEy)M4r zPP%VIW@7*1*M;p(jU9|1T>OTp*s1j!nLfODn^>!}+MMCV+r<%N7=IzWQ8=dfO>uCS zX1b+NE#4_M?9td)g##V?Mac&m`y}VX;sau_&T1W>7Jnux_G-Gy(!<4H2>T}*d&2cg z@evXKsm4OBP;yjUqq8?%rjoD4U}7E&|5Adj zxyE)xoO1juMtq^MPIxu`MJ&_VvHVYq|053SY@h48lHY{upl10>^@+kiMR%PY%sNqc zL2NlhI?qCImJBQTOPpl75a#CJQu4Pb`AX|}L*l=Q+;dE0Lv0&N!sMpo8e0&vvBV-5onYnxqhU`;oSc4IQ@)yY zx+FvH{6S+MWc^x_CEq%$v5LwMi*se~a~kVYyuL6`eyFoe&iK+ox&LQP_f$f0X^~7k zuQ6IrB{EBA8w)B*OJ&F3HJxWra=+4Yx#tg!ws(y$Z7=Kp*4Si6ed$&5`+qbxr{ez7 zj?%;*iY$cIiAzhn$O$@oIdNraSJ@H2mq6?3tHiaX*T}U#hSSttp41tgGd*OK)GR|i zwiAUtwpmdPzr?YP@CkhA4xjOqj%X6Y|hy3;Tm=~(%KpUJWb@+UtFFS|{43L&2jup=r8?vnL7 zJCbgM8FDeRci<4-lX_;zCzxv2qZx7wGkQ{qE1MzrYRoh}=zVCA2XuC*9kIhYTN9H} z)*!$2_p>9zf9Z~fWFpI1@*kb8z(?O%GB}iU6i>h`k4$HJRD5DH!fe?_XQL`JkQIcH z>CupqwvMvdvQ%T@WC>YT=sKlc39|0G?o_K%v|OOG*K@m}&dZF3RE@f4HD+=ckHZ}4 zc}b^}S`vL-XEdfc@*QScrgP*GW?G_i4uc!aKC&+XPYt6`{m<0qeSnQPcqXIyl;}Kp zJu@xfJULO<(G<>?i*!Z-=gayiE#T8_$Cu5Qb2Y~C=F0_9g06o@Lnz>UxlC6M&z*#( zhjm5)=gV)IQDtVJ&R`*@vrNwvABP8JxXyY*4f;`d&(Y^S zD*mgNbb{hy;1j&{6g1_LY*qV z)^!c2Q{^dL_bBQX$)9!ICe$sG7l?Uq6bJCR?O~Y}&0~Ul(!6C4%O{v=sd?oy%(R2X zE4Mb&ZP)vqg`#k;+^Mti;;}NX{7z?u@I=`Xd0uC8VSU+B+1pC-JhLEDtS@_1ZeWV1 z7i=tBCg0cDd$6_aad|{%`C?nya(RxKmf8yWhprnS-YHulBW&bUTNf*(otd@}SIG%F zqjj-L4yG^1LW2itr4i{JOpk_~ZL>qJlJy$nWwJ`n(RH*+R>=o--75Q@vQ_e9ozW^; zCBI~*gwvPIoXH$Lwgx!ZruWu%pH}nbmTd zuA^jD%h|fF+I|o%-_;oKQ=Uyp{b6QZI*;GQZ9^Af5htPY~K-A-+i_}T~BQTtwYt+l;jQBe{-ko;w< z^xMB4MG{I~!UGi-x3}h?#1^1dz-}O;Zr;OL_++}enh5!j5QWhc7}S!6@HOWZD5tU* z;8T{5LmL4XqlEX0ee(a-rdSfJ9t(BQ3APnD#zQ#gBDppnO?c@5eWO{72}x*gq+Z&P zR2~uyKw>C;xu6Of;AjbDT7_f7 zZynaq2%9EEn=Z*qEpY056Gcl<0M%W>P8@My^)IeFejw*7{LftY;kd1Q+oAc2@;;v< z?A~oTBHa5Kf=5eKD=DodOcVP4GOn zL*PB{f>t84+1%*M9rFst`5@PqZVVbz5YL%WxAC1zbi7h90Ud&xPAaV`gub)Rfd$9q z#xr!(gR56wrZ|@yJ{^UfF?Vgm629f5lPoW{%`V;rzFyby8UGJ2-s>_ule-8$m81Rc zd?nRf2Ra(hTV-Y;^%{k#3z(P9=>0WUq$JQnSAgnnfX+BkzLVzxZvp=84A)xWOO1A> z2JsHS116y2mm4?@uqliG);`Ol30%t5#*^O2XWh@1-jx)8rVJvrlwObvh-b3MxY1$q$R$|-dris zJ}Qa6tNE1$9}eN?3W;lNVlFNi6TF^E9klBLIZ~jJ_o=po^hxrH0+HgJxMtXZ;H=Y13f)5{y)H*azUQ63|GB#a{wCjxI6VvaSuU$>cTmT)6cwLGi zpEIJV9gN#TfwLZ5dcG4=+Yy>;Uz^4;fmdOZJINPB1=?bL4ScN+(l6$|UgYz8p0i7l z{7YV63QR8Ykc)k|c!SsxZeZR`nj^Y1((Z-~+#v`%(8)pe!EA40`w&ROqahRwC=Y{V z#BdmZ_dbF3@LeeA?nz5d!s1lCCHE29I|_#}z>}O%Jf8nX_?TCbwzDSk&eAHO?Zj7H za zv|LqYnq3!i68v^I+u%&2W#C;mEnZw79$t;w z%+Wbw&=;U)+O+0)&T|S3Zq^GBxo1 zzHi6VapgO-=vwNVMQu!hsLj20%#vKfRcs&F*0vu>6xdWE4H#^c_oz$6C1{=kPnL8( zFMjY^ip1^m#ze2PNF;jei-alrM!Zc5hNNW5rZ+DgdVr8X%Shn^mhSBL6_MMPO`}w7^nq|BOuU5i>6_* z7ys1qe*BLX3fhqC77NR*IAO3S^z2 zXUuTOXy?FoFunj>Co=GH(FTWdYhbH5gpY^Y(cTEjka05_Ww9ckJCty{g4-Rr?UH1= zj}j5w?}Y$G^Zkz!t_@mMA@IY9k#>YQSW2>Najbd3k#2bqnPP=$Q3EZfd{_{ zFqv^2ypx)XZNl&12K-ZY7d|XSng5|3?U{5NBX$mHu0$6|71b0gYN{ai|rYYetiYU<=$=x2*O3U^lHXUiffVwbeY&Q*Km5}@fQ z7p-GHm3tt~eA@d=nTYfp%1u;`b7n_@-iRLOWcKS$stP75r+H4#>o%KPJfIxpRQE%c z!HaF0u8HE-n&nDYg{pQC<)`HZwkcP}6z^6puw)&SgdJDbK}5m#i2E!wsCbVf#9+WR zST4eGRXU(SSc;BAIIr1(3cV9P3ezJ?4K`e%Tck~lu0DsjH`S{gg+7tv4a1ZV9Fq*m z>>-OiWO3how$B$?hI4YXVl=;CDB(436Esmr;p+lJPzCn#)Idk>b`-XyqzCO19V)0) z_ZQ>_bw_m#^kc0}j7cylCG5_nw8Ap4gI&>{pme2&>)D{e9DW2_elDH~+ZFV=KFd^_ zbU|x>1gB*~%c;h3Xt}^RSq!RncpzV#cAS%wMP9-asG!yAQtaWojng^b*^JXU{B$1o zbn#rmIXN3sIgLouH;)6)k;<>{K4!Hc*CC5)bB z*kO6_GBLBl2sCHQgC{BrQl1XJ05dC|M^oDERJtv4Bc@n_f3`#=bt#Y3e1}>}y@c^z zSc99ZLA+Mf-?WD7v4PXtM5BaFJiHB@;;ZcKElz!phmBt4@$KheuktAO^Z54Y9a=i# zLJnewqLAZg|Ag^0TJ{gwk4Xle(rPeBLRtZFVF@BT zEFJNu==UK(w#U=NrQDZ1GVCZkoHjmeBbI$?SS$2DJM44B{vkEe;an8fRYrJnio)OG zad(v+(yPL|$}LLA@I5#Y{X>!^&2E;YeX#_-j2IHWRk^OkaI~kn->%$OS{I%rY4rK% z)QCv(eC}1ky>=;o$%n%CDKc#_dL9(iamll`1p9h2aMo!SFYpQ0Rqk}|3?C*_ zYCb?5iQgV^DepRtV&~7D--jPnes=y8{+%)=;XmPYDz1sx&x@!#hwR5r`*A{pd6fIb z$&@7X2yS!Npr{Qj z*}#&mc%7RQwH2>$+oCpMFC%P{d%ckrc!NRYRThnw8}RM=tGsYtMRipLK487;h>C7y znBsCrzoqB$W%bNxiZ7tXkW;!P+9ro2WPnZHQR+Y}&pRzAit7v8TK7nrxrt&tUK;ju z-|fnm)kCctWs=KnJ;+mioO8TaR7EbZ-YZJ+gm)>u5|>+DO6$@!RvfD*Y`t}uVVBZq z9c`dY&*RX_#cA%gQ6z`O+BS-ojx8ald6=yX$;HLC^IW@RUT*_sq^q+n!O$;ykj;Q6 z4y{}Ab5^ddRD}uuYVQM^3GX&OMvRBg5Yt$a#S%MsNR$tU5KCCmh7}cPYKCg~8nGj| zV$Aq&2Hb<#9ezjb4eMj^y&u#RnBflSA7{o3eMr0s?nV!0m!ru~s>UI6-k?ohB<41rmlCIuu01=%A|+$0HVc@R?;w z>q4-?48$aO3^5a)M9hOTh{cfBrV!de0b&Pei`b3jeGxn0pHzNe>wu4nAJ~R-dpNge zaeJ2D9u8NvUCol!ELjc1VRYLSg8FU~9{P2J2}o+EG1fB*g(T^WHH^a;8yHtIDu!cd z)4>ppILtuy!@1qS?OBW~8CSD>n_(HFCtYEBWtYxfc6v?Db zXRKiy#@L`qQt;1<+YU=1n+C>}jKf-xyrBijS2AvAtWO;a1?3H%R92_4nsGbhQAUx@ zAsK5JhiQo4lyBX(I*YV5S=8Q^I~JywZ^J*Z*k7JjO!8{RTE-QO+Zc}&uSYGE5LTCX z=vOV1dd3xu+Zdsg9HlWbrt*8{-j{Lu(GqSlfC% zt^ax^D;T#i9$|zwY|2>6SkJhEaT_C)bBc`BjJ1s07>_VQTee}WW~^nbN5u8NLKC&i z1y|b@6%=wCV_GGLXRKwcXI#O!jqwO$ZF{n;XI#O!jqwQM@h+!OyAIAkU$I4eBvNIe ztdbq&NI69oc;D|J-Y%VMdD^ny5*u9;JurG%^mEa#MIVYj6P<0n#=6+L*}B*IiS?}Y zS8I?h*_L6OYMX7l&$i08*86rxG0AgN=?`T9gObi6&JX()aT?>mY--DxKM_34SD;f0}E2 zCV^~vr2SfkFUX#*iNhS%){uIHD;e9<3bPUaZFUD%P-kt9A2Cy}pvpqDf4Zg^aY;}c z#7-52JK`(-?KND{)vY_AWK5f`h{gCH^28U!545DEp_w{c_wcK4Od) zHUs_b8%xM?JkRJmyZ~1e4n;|J1|`~$@ovT~j9+q9#+8jhn@BB}sIm!~yUjIS#5#%p z5y*G(wb@d!3-M9>lNAY%iT4p77kd#`iG7G`#K(wh#V3gC!~w*o#pj66iZ2nL$3GB} z_~dvP@kM-O$IrsVF~pa}3B*^$w}@}z8ypF5iBpI>#Se(P#E*#YdBj=#wOjm*_<{HZ zU%c%_l<=YW72Ep|C43})rx$gI684KfvHc06gipm^h~J5S5KrNIBndy`KWiZ27ZD`z z4``JXd_{JZl8V?-Nk{CYWFmG^vJtOQauB=YZ5qDr8-S)Q;RZBqiT7g#h&Q5XOS~(~ zM!ZRJAdXO40saSe(9#ZWw6x=WnhWtZw6wz=XlaK#75sZjn2wgMpdKw-!A!Jl1+$e> z#Cy=P70gA;R`|#Atr6!bt*!+J>_Jlpe1N77_(-Wl+>fRX_!LbY@R?GD_ysz3!dGbN zgd=F_gm07@#FJ?0gzwSP38$3~h-bW8yNl;M^_DxLJ6Nx?-etYV`j@r6?OI!%%}`Ho zHUod?pXtEwk@$ZaZLi>`3-q4V*mc5 z-6C$ynIj$)oAH}lgG<6fdcrXLcSuG8#CTUsmA1|9w`5^3epZ#d@q;OM=XLAx{B{2f z?Y6h)@y*ZgmhCJ`$KW{cnS-(*#w?{Nx}%J){~^XQURdfs$Nx8b3l7O#rB!f{_t3{O zGuVu>cEKhlo9{X#S7#c`(e>LmrwC=fLA3F%4^r-m?x_DKeW@BZQaf#7daUf7?Y-8h X?A&ZNDSrgX38kAYX5~k*&>8%HhV{>m delta 30552 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 diff --git a/docs/features/serialization/serialization.md b/docs/features/serialization/serialization.md index 39494a660..728cb49c1 100644 --- a/docs/features/serialization/serialization.md +++ b/docs/features/serialization/serialization.md @@ -1678,7 +1678,9 @@ public partial class Player A member that takes a reserved number or a reserved name is `WPROTO043`. **Names are reserved as well as numbers**, for the reason protobuf reserves both: a re-added `Health` at a _different_ number still breaks anything matching by name -- a JSON projection, a generated `.proto` consumer, a schema -registry -- while carrying data that means something else. +registry -- while carrying data that means something else. The check reads the name a consumer +actually sees, so `[WProtoMember(9, Name = "Health")]` is refused whatever the C# member is +called, and a C# `Health` presenting itself as something else is not. A reservation is a record, not a permanent ban. If the removed member really is coming back unchanged, delete the matching `[WProtoReserved]` in the same commit; `WPROTO043`'s message says so, From 8e50890441ae7afbb3c35aca913e82fe746b9f76 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 02:03:19 +0000 Subject: [PATCH 10/15] Prove a reservation cannot change the wire A reservation documents a wire contract, so the one outcome it must not have is changing one. Asserted by comparing the emitted formatter source with and without [WProtoReserved], which is stronger than comparing bytes for a handful of values: if the emitted code is the same code, there is no payload the two could disagree about. 710 generator tests against protobuf-net 3.2.56. Addresses #608 Co-Authored-By: Claude Opus 5 (1M context) --- .../DiagnosticTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs index 601ba34ce..6ae6184ba 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs @@ -1067,6 +1067,35 @@ public void AMemberCannotTakeAReservedName() ); } + /// + /// A reservation is a record, and a record may not touch the wire. + /// + /// + /// Stronger than comparing bytes for a handful of values: if the emitted code is the same + /// code, there is no payload the two could disagree about. A reservation that changed the + /// formatter would be a wire break introduced by documenting a wire contract, which is the + /// one outcome this feature must not have. + /// + [Test] + public void AReservationDoesNotChangeTheEmittedFormatter() + { + const string Members = + @" public sealed partial class Save + { + [WProtoMember(1)] public int Kept; + [WProtoMember(4)] public string Name; + }"; + + Assert.AreEqual( + GeneratedFormatterFor("Consumer.Save", "[WProtoContract]" + Members), + GeneratedFormatterFor( + "Consumer.Save", + @"[WProtoContract] [WProtoReserved(2, 3)] [WProtoReserved(""Health"")]" + + Members + ) + ); + } + [Test] public void AMemberCannotRenameItselfOntoAReservedName() { From 07381776b7ee9c4c94ecc46a05a5eeca2295e66d Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 02:07:02 +0000 Subject: [PATCH 11/15] Put the schema-name helper's documentation on its own method Inserting the helper above AsksForZigZag's signature put it between that method's doc comment and the method, so AsksForZigZag lost its documentation and SchemaNameOf inherited a about ZigZag. Caught by lint-xml-doc-summaries, which exists for exactly this -- a member's documentation outliving the member (#591). Recorded rather than quietly fixed, because the miss was mine twice over: an anchor chosen on the signature rather than the doc block, and a push that ran the targeted gates for the docs I changed but not the one for the C# I added. Co-Authored-By: Claude Opus 5 (1M context) --- .../WProtoGenerator.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs index 848237053..e970a3d83 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs @@ -3399,18 +3399,6 @@ params object[] arguments ); } - /// - /// Reports whether the attribute asks for DataFormat = ZigZag. - /// - /// - /// The member's value is read off the enum's own declaration rather than compared - /// against a constant here. An enum argument arrives as its underlying integer, so a - /// hard-coded 1 would work right up until someone renumbered - /// WProtoDataFormat -- after which every annotated member would silently go back to - /// writing int32, which is a different payload and not a build error. The generator - /// cannot reference the runtime assembly, but it can read the symbol the argument is typed - /// as, which is the same declaration. - /// /// /// The schema name a member declared for itself, or null when it declared none. /// @@ -3434,6 +3422,18 @@ private static string SchemaNameOf(AttributeData attribute) return null; } + /// + /// Reports whether the attribute asks for DataFormat = ZigZag. + /// + /// + /// The member's value is read off the enum's own declaration rather than compared + /// against a constant here. An enum argument arrives as its underlying integer, so a + /// hard-coded 1 would work right up until someone renumbered + /// WProtoDataFormat -- after which every annotated member would silently go back to + /// writing int32, which is a different payload and not a build error. The generator + /// cannot reference the runtime assembly, but it can read the symbol the argument is typed + /// as, which is the same declaration. + /// private static bool AsksForZigZag(AttributeData attribute) { foreach (KeyValuePair argument in attribute.NamedArguments) From 62380f1e15caed4de2fcf5eaf0ceae9c692aa9e1 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 02:17:57 +0000 Subject: [PATCH 12/15] Refuse an empty corpus in three more gates, mine included The empty-corpus rule was enforced by test-empty-corpus-gates.ps1 over six PowerShell linters and had never reached the JavaScript ones. Measured by pointing each at a directory holding nothing, through the scan-root override its own self-test already uses: lint-comparison-direction exit=0 lint-nested-type-placement exit=1 lint-xml-doc-summaries exit=0 lint-doc-identifiers exit=0 Three of four reported a clean run over nothing, including the linter this branch added an hour earlier -- the session's own theme, in the session's own new code, for the third time. All three fail an empty walk now and name what was empty. lint-doc-identifiers checks both halves, because either going empty makes every answer vacuous: no documents means nothing was read, and no namespaces means every using would resolve to nothing. It reports its document count on success too, so a corpus quietly shrinking is visible before it reaches zero. A red half per gate, in each gate's own self-test rather than in a central list, so the case sits beside the rules it guards. Addresses #556's rule for the JavaScript family Co-Authored-By: Claude Opus 5 (1M context) --- scripts/lint-comparison-direction.js | 11 ++++++ scripts/lint-doc-identifiers.js | 34 +++++++++++++++++-- scripts/lint-xml-doc-summaries.js | 12 +++++++ .../tests/test-lint-comparison-direction.js | 20 +++++++++++ scripts/tests/test-lint-doc-identifiers.js | 34 +++++++++++++++++++ scripts/tests/test-lint-xml-doc-summaries.js | 20 +++++++++++ 6 files changed, 128 insertions(+), 3 deletions(-) diff --git a/scripts/lint-comparison-direction.js b/scripts/lint-comparison-direction.js index 5c9c9b380..1a12029c8 100644 --- a/scripts/lint-comparison-direction.js +++ b/scripts/lint-comparison-direction.js @@ -835,6 +835,17 @@ function main(argv) { } return 1; } + if (files.length === 0) { + // A walk that matched nothing is the absence of a measurement rather than a pass: a renamed + // source root, a moved tree, or a walk that stopped descending all reach this line otherwise + // (#556). Reported whatever the verbosity, because a silent zero is the whole defect. + console.error( + `[comparison-direction] no C# files were found under ${SCAN_ROOTS.join(", ")}, so this run ` + + `checked nothing.` + ); + return 1; + } + if (verbose) { console.log(`[comparison-direction] ${files.length} file(s) clean.`); } diff --git a/scripts/lint-doc-identifiers.js b/scripts/lint-doc-identifiers.js index 573dd66e9..d82e455be 100644 --- a/scripts/lint-doc-identifiers.js +++ b/scripts/lint-doc-identifiers.js @@ -135,9 +135,11 @@ function analyze(root) { const violations = []; let usingCount = 0; let assemblyCount = 0; + let documentCount = 0; for (const docRoot of DOC_ROOTS) { for (const file of filesUnder(path.join(root, docRoot), [".md"])) { + documentCount++; const relative = path.relative(root, file).split(path.sep).join("/"); const lines = fs.readFileSync(file, "utf8").split("\n"); lines.forEach((line, index) => { @@ -168,11 +170,37 @@ function analyze(root) { } } - return { violations, usings: usingCount, assemblies: assemblyCount }; + return { + violations, + usings: usingCount, + assemblies: assemblyCount, + namespaces: namespaces.size, + documents: documentCount + }; } function main() { - const { violations, usings, assemblies } = analyze(SCAN_ROOT); + const { violations, usings, assemblies, namespaces, documents } = analyze(SCAN_ROOT); + + // A walk that matched nothing is the absence of a measurement rather than a pass: docs/ renamed, + // a source root moved, or a walk that stopped descending all reach the success line otherwise + // (#556). Both halves are checked, because either one going empty makes every answer vacuous -- + // no documents means nothing was read, and no namespaces means everything would resolve to + // nothing and be reported, or, as here, nothing would be reported at all. + const empty = []; + if (documents === 0) { + empty.push(`no Markdown files under ${DOC_ROOTS.join(", ")}`); + } + + if (namespaces === 0) { + empty.push(`no namespaces declared under ${SOURCE_ROOTS.join(", ")}`); + } + + if (0 < empty.length) { + console.error(`[lint-doc-identifiers] ${empty.join(" and ")}, so this run checked nothing.`); + process.exitCode = 1; + return; + } if (0 < violations.length) { console.error( @@ -190,7 +218,7 @@ function main() { // here rather than printing the same success line a clean corpus does. console.log( `[lint-doc-identifiers] ${usings} package using directive(s) and ${assemblies} assembly ` + - `reference(s) in docs all resolve.` + `reference(s) across ${documents} document(s) all resolve.` ); } diff --git a/scripts/lint-xml-doc-summaries.js b/scripts/lint-xml-doc-summaries.js index ff4105ed2..aa9b77fe9 100644 --- a/scripts/lint-xml-doc-summaries.js +++ b/scripts/lint-xml-doc-summaries.js @@ -289,6 +289,18 @@ function main() { return; } + if (files.length === 0) { + // A walk that matched nothing is the absence of a measurement rather than a pass: a renamed + // source root, a moved tree, or a walk that stopped descending all reach this line otherwise + // (#556). Reported whatever the verbosity, because a silent zero is the whole defect. + console.error( + `[xml-doc-summaries] no C# files were found under ${SCAN_ROOTS.join(", ")}, so this run ` + + `checked nothing.` + ); + process.exitCode = 1; + return; + } + if (verbose) { console.log(`[xml-doc-summaries] ${files.length} file(s) clean.`); } diff --git a/scripts/tests/test-lint-comparison-direction.js b/scripts/tests/test-lint-comparison-direction.js index a63c3cc00..84fc9433c 100644 --- a/scripts/tests/test-lint-comparison-direction.js +++ b/scripts/tests/test-lint-comparison-direction.js @@ -150,6 +150,26 @@ runTest("fix: refuses a comparison that spans more than one line", () => { assert.strictEqual(fixed(source), source); }); +runTest("a corpus with no C# files is rejected rather than reported clean", () => { + // A walk that matched nothing is the absence of a measurement, not a pass (#556). Reachable + // with nothing looking wrong: a renamed source root, a moved tree, a walk that stops descending. + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "comparison-direction-empty-")); + try { + const result = spawnSync(process.execPath, [linterPath], { + env: { ...process.env, COMPARISON_DIRECTION_ROOTS: directory }, + encoding: "utf8" + }); + + assert.strictEqual(result.status, 1, "an empty walk must not report a clean run"); + assert.ok( + result.stderr.includes("checked nothing"), + `the report must say what was empty, got: ${result.stderr}` + ); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); + runTest("linter exits non-zero on a violation and zero once fixed", () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), "comparison-direction-")); try { diff --git a/scripts/tests/test-lint-doc-identifiers.js b/scripts/tests/test-lint-doc-identifiers.js index 34ab6a400..dd9d04bb7 100644 --- a/scripts/tests/test-lint-doc-identifiers.js +++ b/scripts/tests/test-lint-doc-identifiers.js @@ -194,6 +194,40 @@ test("every violation is reported, not just the first", () => { assert.ok(/2 documentation reference\(s\)/.test(output), output); }); +test("a corpus with no documents is rejected", () => { + // A walk that matched nothing is the absence of a measurement, not a pass (#556). Reachable with + // nothing looking wrong: docs/ renamed, a tree moved, a walk that stopped descending. + const { status, output } = run({ sources: NAMESPACE_SOURCE, docs: {} }); + + assert.strictEqual(status, 1, output); + assert.ok(/no Markdown files/.test(output), output); +}); + +test("a corpus with no declared namespaces is rejected", () => { + // The other half. With nothing indexed every using would resolve to nothing -- so the run would + // either report all of them or, as it did, report none and exit 0. + const { status, output } = run({ + sources: {}, + docs: { "guide.md": "```csharp\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n```\n" } + }); + + assert.strictEqual(status, 1, output); + assert.ok(/no namespaces declared/.test(output), output); +}); + +test("a passing run reports how many documents it read", () => { + const { status, output } = run({ + sources: NAMESPACE_SOURCE, + docs: { + "a.md": "```csharp\nusing WallstopStudios.UnityHelpers.Core.Attributes;\n```\n", + "b.md": "# no code here\n" + } + }); + + assert.strictEqual(status, 0, output); + assert.ok(/across 2 document\(s\)/.test(output), output); +}); + test("the repository itself passes", () => { const result = spawnSync(process.execPath, [LINTER], { encoding: "utf8" }); assert.strictEqual(result.status, 0, `${result.stdout || ""}${result.stderr || ""}`); diff --git a/scripts/tests/test-lint-xml-doc-summaries.js b/scripts/tests/test-lint-xml-doc-summaries.js index b6eb81e8b..63cf26284 100644 --- a/scripts/tests/test-lint-xml-doc-summaries.js +++ b/scripts/tests/test-lint-xml-doc-summaries.js @@ -209,6 +209,26 @@ runTest("CR-only source is analyzed the same as LF", () => { assert.strictEqual(analyzeFile(orphanedBlock.replace(/\n/g, "\r")).length, 1); }); +runTest("a corpus with no C# files is rejected rather than reported clean", () => { + // A walk that matched nothing is the absence of a measurement, not a pass (#556). Reachable + // with nothing looking wrong: a renamed source root, a moved tree, a walk that stops descending. + const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "xml-doc-summaries-empty-")); + try { + const result = spawnSync(process.execPath, [linterPath, "--verbose"], { + encoding: "utf8", + env: { ...process.env, XML_DOC_SUMMARY_ROOTS: scratch } + }); + + assert.strictEqual(result.status, 1, "an empty walk must not report a clean run"); + assert.ok( + result.stderr.includes("checked nothing"), + `the report must say what was empty, got: ${result.stderr}` + ); + } finally { + fs.rmSync(scratch, { recursive: true, force: true }); + } +}); + runTest("the linter exits non-zero on a fixture tree that violates the rule", () => { const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "xml-doc-summaries-")); try { From 0bc6d1fcdb7454ab962f4d00d31592f04d78f301 Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 02:44:25 +0000 Subject: [PATCH 13/15] Expose the reserved sets as read-only, matching the attribute precedent WGroupEndAttribute already exposes IReadOnlyList for the same shape, so an attribute handing back its own mutable array was the odd one out. The sets are constructed in the constructor and never change; the type now says so. Verified the schema exporter still renders identically -- reserved numbers and names in the same order, and the three out-of-range diagnostics unchanged -- by re-running the real WProtoSchemaText against the same fixtures. Co-Authored-By: Claude Opus 5 (1M context) --- .../Serialization/WallstopProto/WProtoReservedAttribute.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs b/Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs index 33b116df3..73b910f93 100644 --- a/Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs +++ b/Runtime/Core/Serialization/WallstopProto/WProtoReservedAttribute.cs @@ -4,6 +4,7 @@ namespace WallstopStudios.UnityHelpers.Core.Serialization.WallstopProto { using System; + using System.Collections.Generic; using UnityEngine.Scripting; /// @@ -92,9 +93,9 @@ public WProtoReservedAttribute(string memberName, params string[] alsoReserved) } /// The field numbers this declaration holds; empty when it reserves names. - public int[] FieldNumbers { get; } + public IReadOnlyList FieldNumbers { get; } /// The member names this declaration holds; empty when it reserves numbers. - public string[] MemberNames { get; } + public IReadOnlyList MemberNames { get; } } } From 68c39daac895d1bdc8d99aa82cf40c2aea126dfb Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 03:37:01 +0000 Subject: [PATCH 14/15] Reserve only the name a consumer sees The rule read the C# identifier alongside the schema name, so a member presenting a free name through [WProtoMember(Name = ...)] was refused when its identifier happened to be reserved. That contradicted this feature's own documentation, which already promised "a C# Health presenting itself as something else is not" refused, and the commit that introduced it, whose subject was reserving the name a consumer sees. The test could not have caught it: it named the member Vitality AND set Name = "Vitality", so it passed whichever of the two identities the rule happened to read. It now reserves "Health", names the member Health, and presents it as Vitality -- the only arrangement that tells them apart. Verified by mutating the rule back: that case is the one that goes red. Reported by Cursor Bugbot. 710 generator tests against protobuf-net 3.2.56. Addresses #608 Co-Authored-By: Claude Opus 5 (1M context) --- .../DiagnosticTests.cs | 9 +++++---- .../WProtoGenerator.cs | 18 ++++++++---------- ...opStudios.UnityHelpers.Proto.Generator.dll | Bin 200704 -> 200704 bytes 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs index 6ae6184ba..e34538870 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs @@ -1115,14 +1115,15 @@ public void AMemberCannotRenameItselfOntoAReservedName() [Test] public void RenamingAwayFromAReservedNameIsAllowed() { - // The other direction, so the rule is about the name that reaches a consumer rather - // than about the identifier: a C# member called Health presenting itself as something - // else is exactly what the escape hatch is for. + // The identifier here IS the reserved word and the schema name is not, which is the + // only arrangement that can tell the two identities apart -- the first draft named the + // member Vitality as well, so it passed whichever name the rule happened to read. + // Reported by Cursor Bugbot. CollectionAssert.IsEmpty( Run( @"[WProtoContract] [WProtoReserved(""Health"")] public sealed partial class Save { - [WProtoMember(9, Name = ""Vitality"")] public int Vitality; + [WProtoMember(9, Name = ""Vitality"")] public int Health; }" ) .Select(diagnostic => diagnostic.Id + " " + diagnostic.GetMessage()) diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs index e970a3d83..3e4f2e315 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/WProtoGenerator.cs @@ -3220,17 +3220,15 @@ NestedCollections nested // fix the author can see in front of them; a collision with something deleted needs // the reservation explained. // - // Both names, because [WProtoMember(9, Name = "Health")] presents itself downstream - // as Health whatever the C# member is called -- and a rule that read only one of - // them is one an author steps around by renaming. + // The name a CONSUMER sees, and only that. A generated schema, a payload dump and + // anything matching by name all read [WProtoMember(Name = ...)] where it is set and + // the member's own name where it is not, so that is the identity a reservation + // protects. Reading the C# identifier as well would refuse a member presenting a + // free name, which is the decoupling Name exists for. string schemaName = SchemaNameOf(attribute) ?? symbol.Name; - bool reservedName = - reserved.ReservesName(symbol.Name) || reserved.ReservesName(schemaName); + bool reservedName = reserved.ReservesName(schemaName); if (reserved.ReservesNumber(tag) || reservedName) { - string takenName = reserved.ReservesName(symbol.Name) - ? symbol.Name - : schemaName; Report( context, WProtoDiagnostics.ReservedTag, @@ -3239,9 +3237,9 @@ NestedCollections nested symbol.Name, reserved.ReservesNumber(tag) ? reservedName - ? "field number " + tag + " and the name '" + takenName + "'" + ? "field number " + tag + " and the name '" + schemaName + "'" : "field number " + tag - : "the name '" + takenName + "'" + : "the name '" + schemaName + "'" ); failed = true; continue; diff --git a/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll b/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll index 4da2208d305a5b996f62955db2bcafb7bbca5b31..b858dd8d7f9eaf671c82289643c63af2aede8c12 100644 GIT binary patch delta 10070 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 delta 10091 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 From afd46ef444af5415c2191839562f5d15d27589be Mon Sep 17 00:00:00 2001 From: wallstop Date: Sun, 30 Aug 2026 05:46:32 +0000 Subject: [PATCH 15/15] Say why cross-assembly subtypes are refused, not that they always will be WPROTO040's message claimed the limit was "how per-assembly generation works rather than a gap waiting to be filled". The first half is true; the second overreached, and three measurements taken after that wording shipped show a mechanism that could lift it: - A consumer's compilation can already read the package base's [WProtoInclude] tags and every [WProtoMember] tag out of metadata, so collision detection needs no new information. - WProtoFormatterProvider already documents that a later registration replaces an earlier one, deliberately, so a consumer can supply a base's formatter. - The emitted chain is ordinary static code; nothing requires the assembly that declared the base to be the one that emits it. So the extending assembly could emit the base's whole chain itself. That is not the runtime registry #603 refused -- no lookup, no MakeGenericType, no unordered-registrar hazard -- and it is now tracked on #612. The message states the mechanism and the working alternative and stops predicting the future in either direction: it does not promise a release either, because the refusal is real today whatever #612 decides. The test that pinned the permanence claim now pins the mechanism and the fix instead. 710 generator tests against protobuf-net 3.2.56. Refs #603, #612 Co-Authored-By: Claude Opus 5 (1M context) --- .../DiagnosticTests.cs | 30 +++++++++++------- .../SubtypeMap.cs | 28 +++++++++------- ...opStudios.UnityHelpers.Proto.Generator.dll | Bin 200704 -> 200704 bytes docs/features/serialization/serialization.md | 30 +++++++++++------- 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs index e34538870..ab0a19396 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator.Tests/DiagnosticTests.cs @@ -514,18 +514,21 @@ public void ASubtypeCannotDeclareItselfAgainstABaseInAnotherAssembly() } /// - /// The refusal states a permanent reason and a fix, rather than implying a future release. + /// The refusal explains the mechanism and names a fix that works. /// /// - /// The decision recorded on - /// #603. - /// The alternative is a runtime registry whose every failure mode -- Unity's registrars - /// running unordered, two packages claiming one tag on a shared base, a lookup stripped - /// under IL2CPP -- is silent data corruption rather than a build error. A developer reading - /// this message needs to know it will not change, and what to write instead. + /// A developer whose build just failed needs two things: why, and what to write instead. + /// The "why" is a fact about per-assembly generation -- the base's chain was emitted when + /// the base's assembly compiled -- and NOT a claim that the feature can never exist: + /// emitting the chain in the extending assembly is a different mechanism entirely, and is + /// tracked on + /// #612. + /// The runtime registry refused on + /// #603 + /// is the thing that stays refused. /// [Test] - public void TheCrossAssemblyRefusalNamesAPermanentReasonAndAWorkingAlternative() + public void TheCrossAssemblyRefusalExplainsTheMechanismAndNamesAWorkingAlternative() { MetadataReference upstream = CompileReference( "UpstreamAssembly", @@ -540,11 +543,16 @@ public void TheCrossAssemblyRefusalNamesAPermanentReasonAndAWorkingAlternative() .Single(diagnostic => diagnostic.Id == "WPROTO040") .GetMessage(); - StringAssert.Contains("rather than a gap waiting to be filled", message); + // The mechanism, so the reader can tell this from a number they merely chose badly. + StringAssert.Contains("generated when its own assembly is compiled", message); + + // And the shape that does work, because a diagnostic naming no fix is half a report. StringAssert.Contains("[WProtoMember]", message); - foreach (string implication in new[] { "not yet", "for now", "until", "in a future" }) + + // It must not promise a release either. The refusal is real today whatever #612 does. + foreach (string promise in new[] { "not yet", "for now", "in a future", "will be" }) { - StringAssert.DoesNotContain(implication, message); + StringAssert.DoesNotContain(promise, message); } } diff --git a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs index 1947a96db..7376fa086 100644 --- a/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs +++ b/Generator~/WallstopStudios.UnityHelpers.Proto.Generator/SubtypeMap.cs @@ -383,14 +383,19 @@ out string problem ) ) { - // Permanent, and the message says so. A per-assembly generator emits the base's - // dispatch chain when the base's assembly compiles; a subtype declared afterwards - // in a referencing assembly is not late to a list, it is outside the compilation - // that built the list. Closing the gap needs a runtime registry whose every - // failure mode -- unordered registrars, two packages claiming one tag, a lookup - // stripping under IL2CPP -- is silent data corruption rather than a build error, - // which is a worse trade than this refusal - // (https://github.com/Ambiguous-Interactive/unity-helpers/issues/603). + // A per-assembly generator emits the base's dispatch chain when the base's assembly + // compiles, so a subtype declared afterwards in a referencing assembly is not late + // to a list -- it is outside the compilation that built the list. That is a fact + // about THIS mechanism, and the message says only that. + // + // A runtime registry would close the gap and is refused: unordered registrars, two + // packages claiming one tag, and a lookup stripping under IL2CPP are all silent + // data corruption rather than build errors + // (https://github.com/Ambiguous-Interactive/unity-helpers/issues/603). Emitting the + // base's chain in the EXTENDING assembly instead is neither a registry nor + // refused, and is tracked on + // https://github.com/Ambiguous-Interactive/unity-helpers/issues/612 -- so the + // message must not tell a developer the feature can never exist. problem = "'" + baseType.Name @@ -401,10 +406,9 @@ out string problem + "' into '" + (subType.ContainingAssembly == null ? "?" : subType.ContainingAssembly.Name) + "'. The base's dispatch chain is generated when its own assembly is compiled, " - + "so a subtype declared afterwards in an assembly that references it can never " - + "appear in that chain. This is how per-assembly generation works rather than a " - + "gap waiting to be filled, and accepting the declaration would compile and " - + "then throw on the first save. Either move '" + + "so a subtype declared afterwards in an assembly that references it cannot " + + "appear in that chain, and accepting the declaration would compile and then " + + "throw on the first save. Either move '" + subType.Name + "' into '" + (baseType.ContainingAssembly == null ? "?" : baseType.ContainingAssembly.Name) diff --git a/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll b/Runtime/Analyzers/WallstopStudios.UnityHelpers.Proto.Generator.dll index b858dd8d7f9eaf671c82289643c63af2aede8c12..914fb2c3bd4853ea215b7b01ce84a5a6f57c9022 100644 GIT binary patch delta 4490 zcma)AdstM}7C(ERc_Radfq@xD2m~2Lh+HB*L++>)3Tdic-K1ffMou-d>=p7UccX3d+l}h z?0tr!)%}WA_bXqhdd=C^xU|$p-;Z0m@hLKb6Oy2|$BBZC$IxW{EJ>g%RJ@oJTIPTg ze@4MC71YAf2qKILpaQ8Js}-~5F*Lj+@trAxvpPOHP;`tG1Or$`w#MxWGOEUSox$w( z6uz`{9L=A~&zDZ6m#6a4WtPBOQ!zMFRUL0*_sO%$QV8kC*Swoa_vOnS?}|7&zT_Vf zZ%#*9xTJ|s(F|n2J%^(R_VsjLS{|8lLLnnVboNC`0XB9HJ@)5eWYUM443fs)26nQZ zU7F6@%Qr)EUc7Yq$lEWwvl{Qpie8Hpl&qJ6l=73*o++)&J6CY8x%lLYaFWgcR`E>e z{WCnVo@HCv>>2#u6{BJrwfSzRHJdk~^s{$*=WMvH(nBcv`s^pma{b+_jM8B`4L5(1PwWR*E*;agy@K+w!r zynRYui@`~61Ni_#I|2l|R+(ML7NU(Xa*^Wg6Z}sMwk`%T6=4;Ep90sREUgU4;Ru}w zeq`%WcB~vp1XtEl|GEvR+g$ApbO=>0%WMmUGyXV&c~ zyL<@9I|$Pcdt??aR-1CZYlq<_gqOed5O-DGi1BTmK%PMue_WCAylS&ccN%OP!ZHLu zlYfQ5OU?pWgOKxsf7uR{?YaQuF@%v9J+iybt0t5!xB}z{2&tN@{&mf$TYL@3Dg^Cy zMaHEXbIzML!7fL*hTy0BEg0PJ3y^IHF}D;MpIBp_cqI(1CC^Y&qt^ZkAF?i#Ke-{3 z9#Qd<4d+Q9e{JI!k{~y497*+WsfiPAw@qpu@^N#o(aDeVgZBYvcM(C8oGBSufkqzx z$xULw2)KzcWL#~*p#EBqC?!G1PWuK{9b2RGGJ!glua~dY_E$lXJg6?&m|`GKM;WA+ z0@3Uy4vo2tsdSJtJ|j|}`!T{5jQ$am_Tgzi(zuhj<PCug6h>>Dgz&{PY*2Q!jvD>-du(XqX^DkMJUQ7vRxhm0HLBD0o=n zr(65;Iosc+vnBb?_C)LePil<8M&vaqLtunug!r-^0{!(P|J+XHIYLtdZG@i4TMRo*pjF!TJTBof1yH4RU{d;O8# z_8x&KZ+ovRwnbANZ)L-{zBx~)JR|mDI4nGkzuVm3QX}3T5zh^ghI}k|Y(YiRQoWj;8aXdDNb0+8E6r*b^36qv*cGdQhhnkQ7c)Ej#v1BJ^0ad}yze zsIU-TvUfON_)R{&7|nnB=C5>Z6whlbQLVoRsEMs{75mFwzLP-%2ZLup8rA{|CACIb zg@z@@cpa?6XL`8oSRXgF$9$*9Jkw)#^_XV|K$rphDW(Ky*j4`E!7w^4jt@R?58W8Y zpEywR{}wkd-dBKapx5*iSLk!FPkPL830@iNRovYR$;RR|%>PQx^`uZ%qHGMcZzY-2 z;=EJztz@6w%k`dq#vp$D;7;-ouR2t$Kc7ULk`^`uuRR>kA3q$XuTKV#&g-c-97ao1 z_=dx&gL*A-a;i76XVOhR2V0g_al}f`#8w=E&+35?UVcE5pE>f92nQW+`mThYO5>wD zMvz!u(eVO)Y5b|fMduIZ^ExeZ>Cqd6em+>9*J&cb!Vu*O+h)M8Q{gO>uRT5vt_%L( z=)v`qy@=vG3;cHw54>&uWHfHeIXNS=v8S<91YMONhf)Yan`+aZ-r+iD5 zVj{!N*;T&on9Qqf4%vjSer}u>6>Q$#Luzx zQZAqUb0)o(D{ubUskRFD5@EpjaYF4uA_T)RIbh`YM@GQk9S7y1w_AT9QhoPzGLWce z3J#&5GVKOQs_(u*GLHuDze1{p7>+)=!2HJVf$IzItsn6X-JENj;rTBKq!1RwRd#Dc iE1eTpY1N81D<^5i&Gkl|_?wz8&#ae>Vi#FDSo=S%lrIAS delta 4656 zcma)A3s{s@8vf6jD>E|8KLZ29br69OW{|5Qww7h7TcD-xqiLy`phe7ZkZhk#1~0jV zNW#%Yb4|cp)2z@xLbEnO5{=4GON-1)t=3-hl2%)7?)Uw3fn=V2Rvuoy^S$r)o$q}A znREWJx@kyt(~z}KD{mK{ZLP13rMD;7@BAIP%n3Nlpk=xF!gK4lDz^+v^H5D5v%cYfzq?2_D9Q8fB zF)HN|ltqf_WT!fX{Wu#(QOx}auU#ACTqTn+CawJ$xqw(!haUR_FfxaPn{}ev{yx}_ z5Vr0Seq!w|NY0nnZ|YH1p48955H3*APk0@!vPh z2>+_o7wcPgG_yU%|JE=ualO1wNE#qUm!UL9G~?1;WxR;r8ARplj~I@sVJ_)4{#z1e zf@JoGU`1&~l+ASW(DhMtrdvu~FB1BITPpZds{D6;<9UT;NV(uMeqxWV%Wq;}NP5 z8X@2&Srf|Ucz~=xID!yF){L@U)j%FV$jDq4T-SoSZOeh&haj$yWjyP7OMdoBu zMF=v<3mE*-Q$T)=P_;^yanJJ>&x&WzMz|tdgBS7_4DMPDic<&^YcXK3Y&*(6Sp($1 z5LVU&m%WIx74=9Wc+SY?;B-4s$2dq_LDH^Z6Ew=Yeftmm^$62on4%2D@JbawWo# z2tj16DC>C%$TJ9|b_JL1Mw#waAmb4-oYCmK@->9C9-l1o*XiCs+4mm+sXq!(hY)1e{U}?1 z49Fb_=Hot@l`C4z`PCo6a2-PU$3Ehws^7x+C4E4yM!10h4J>N0c=ntH`w+s|bFz2P zlHbPQ(O&>rh;SYuNR0dP5PA?Gc+OVKoOM5e{UU( zAafi+*_#UDW*;IHC;=KPq!U}WE2v5(E!{2=<2V&@E5sq9D4Ilr#{MYJ+8)lQ?Z~A| zRlH`$1=7yTcTOR_Qs>SIG{mVUZn)j1tGVgr&HWJI522zWm zOrhtK+Qyl4^iZ~I4%|`UuDN(qP15ttZP|3Pp7*vn=p?;#qs>idrCKU)4<&S-o>%Y9 z$>k8`g3Y2+Xj~)UYLaax`MoKc$QgQ#!W9ivb~|M~K&bhKj%;q-S4#T$s(nd0*HMhM z(AYJ_fJnjqp+u#g>JlZB;upRj`e8=|*;sfq%Ads7@b-L=4uMcrYUL1Pr}t z*dilOdt6iy)}kPB7(*+#BiBVU-%!SQK1_knTAC z#QvwK%_QB}pN1Xa>2D=sBZ}U-N0HNG^3TE^G!r*$!SG-?2bA{%fB!%XHt_EUGNx>@ zKsYuq#O?u2VXy15#BwyH(PI~ydjytk%UVn_g zy-OhqwvgQ3O|orOwoHZTXq*2+&dFb~U!B zpmXRcI}bj2=$)nDdwgYQDGB~ncsG|p>ol<}jv3I6>ZRrw$t<(rqY# zcDe1dV4ieQ$qH@!KZg@&x{b#lNuVV*e(RBlm_k`s3<+hOTtIpxMYZHQGMCU5HmUbr zH&J3Cyyo3I_|xw#q};}@zIQ(@O5jEB*C_8x1ZqzAu!axTdIBed8V&|u>1tL63OTh} zUWJ;S2sqfXp*L~o0uC0I_EX{1tJww|;z-Q~=#9xd^QetZ zOXl|-t@(e8O-~7=IutN{3$dpLxEcNCfBMYRBcb_YB z0LPAj4lZFh?E1GdmCK?mEgQMi9Qb@B@%^7o!&6v1ZKiN5rkwsWq4(zSuFr3$B{^Js zX0_puIq-3K0`?9xWOEL0KJ&}ez�HLiYh}R;S+~s`_tCaG&$K9pDb!o#58w%IXMk zo8a2?-pCr3lPg_48wua@;0`>$h?eB?$Ip+ZOLL{&=T$UsH3X%Z1p9B`ly-hZ<#WfW zW*gDXwjq|${sZ4$1+_AX|KrPVCZ7Q^+8z&ajzUrCy^GB%m^?HuK!mD7?lr(i96a|% zrin`V+O{u;Bo3>W`VJ_FF^=T+3rmOM2oC(JPvP|iPq|T}`BKF{I|$7hEsgx< zb3)Ea5tpwhVzIsU1#oCNV-<2!y@R0&XeA$d?F8io{D*7f6!(uI{OmOgzwi1`x^4`A z{CX~ZZj7|+x?2^!YYY*RrcV`S_Y%Pf=V;P|>66C8-_l-btq*{O+Dt3rCcu<&3tsZ|I@B3QSSH|IUG>2-#@c>Ju$=SaM^TT5+x2pjG^+qT}=0 MMT6oJsn68>7nH0ckN^Mx diff --git a/docs/features/serialization/serialization.md b/docs/features/serialization/serialization.md index 728cb49c1..eea33927c 100644 --- a/docs/features/serialization/serialization.md +++ b/docs/features/serialization/serialization.md @@ -1614,18 +1614,24 @@ identify a type that is really as many types as it has closures. Anything else i ##### Why a hierarchy cannot cross an assembly boundary -The same-assembly rule is where this feature stops, and it stops there permanently. The base's -dispatch chain is generated when the base's own assembly is compiled, so a subtype declared -afterwards, in an assembly that references it, is not late to a list -- it is outside the compilation -that built the list. **The manifest does not change this**, and neither would a bigger one: two -packages that never see each other cannot agree a number. - -Closing the gap would need a runtime registry, and every one of its failure modes is silent data -corruption rather than a build error. Unity's registrars run unordered, so a serialize that happens -before every registrar has run writes under the wrong number or none. Two unrelated packages picking -the same number on a shared base is undetectable at build time and type-confusing at read time. And -a registry lookup has to stay IL2CPP-safe and survive managed stripping. A build error you can see is -a better trade than a player that writes an unreadable save, so the refusal stands. +The same-assembly rule is where this feature stops today. The base's dispatch chain is generated when +the base's own assembly is compiled, so a subtype declared afterwards, in an assembly that references +it, is not late to a list -- it is outside the compilation that built the list. **The manifest does +not change this**: a number was never the obstacle, and writing one by hand does not help. + +One way of closing the gap is refused outright. A **runtime registry** has failure modes that are all +silent data corruption rather than build errors: Unity's registrars run unordered, so a serialize +before every registrar has run writes under the wrong number or none; two unrelated packages picking +the same number on a shared base is undetectable at build time and type-confusing at read time; and +the lookup has to stay IL2CPP-safe through managed stripping. A build error you can see is a better +trade than a player that writes an unreadable save. + +A second way is **not** refused, and is tracked on +[issue 612](https://github.com/Ambiguous-Interactive/unity-helpers/issues/612): the extending +assembly emits the base's whole dispatch chain itself, package subtypes included, and registers it in +place of the shipped one. Its compilation can already read every field number the base spends, so a +collision is a build error rather than a runtime surprise, and the dispatch stays the same static +code. Until that exists, `WPROTO040` refuses the declaration. Two shapes work instead. Keep the hierarchy inside one assembly -- or, when the base belongs to somebody else, **compose rather than derive**: