Skip to content

SpotBugs correctness: SpellCasterChoiceSet symmetry + DescriptionActor null PC - #7671

Merged
Vest merged 2 commits into
PCGen:masterfrom
Vest:spotbugs-symmetry-and-null-pc
Jul 22, 2026
Merged

SpotBugs correctness: SpellCasterChoiceSet symmetry + DescriptionActor null PC#7671
Vest merged 2 commits into
PCGen:masterfrom
Vest:spotbugs-symmetry-and-null-pc

Conversation

@Vest

@Vest Vest commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Fixes two real correctness bugs surfaced during a SpotBugs investigation in the choiceset and output.actor packages.

SpellCasterChoiceSet: symmetry / hashCode contract

The parent ChoiceSet.equals accepts any ChoiceSet<?> (pattern matching on the parent type) and compares setName + pcs. The pre-fix subclass override required the other side to be a SpellCasterChoiceSet, breaking symmetry:

  • parentChoiceSet.equals(spellCaster) returned true when setName + pcs matched.
  • spellCaster.equals(parentChoiceSet) returned false (rejected by instanceof SpellCasterChoiceSet).

hashCode was inconsistent in the same way: parent hashed setName ^ pcs, subclass hashed types and primitives. Equal-by-parent objects could land in different hash buckets.

The subclass's types / primitives fields are not part of identity beyond what the parent already captures via the typePCS passed to super(...). So the right pattern (already used by the sibling ChoiceSet.AbilityChoiceSet in #7628 / commit a7cd6c1) is to delete the override and inherit the parent equality. The class gets @SuppressFBWarnings(EQ_DOESNT_OVERRIDE_EQUALS) with the rationale.

SpellCasterChoiceSetEqualsTest pins the symmetry contract; it would have failed against the pre-fix override.

DescriptionActor.process: null guard

PlayerCharacterTrackingFacet.getPC documents (lines 40-47) that it may return null in unit-test paths where no PC has been associated with the CharID. SpringHelper.getBean also returns null when no bean is registered for the requested type. Both nulls used to flow straight into desc.getDescription(aPC, ...), producing an NPE downstream.

SpotBugs flagged this as NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE at DescriptionActor.java:71. The fix early-returns an empty wrapped string for the no-PC case, matching the existing 'no benefits' early-out a few lines above.

SpotBugs delta

Measured with the project exclude filter (code/standards/spotbugs_ignore.xml, reportLevel = LOW), comparing the branch against its parent commit:

  • EQ_OVERRIDING_EQUALS_NOT_SYMMETRIC: 1 -> 0 (SpellCasterChoiceSet cleared)
  • NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE: DescriptionActor.java:71 finding removed
  • No new findings introduced.

Historical context for deleting the equals/hashCode override

For reviewers wondering why the override existed and whether removing it loses anything — traced through git history:

  • 2008, commit 6f6abda (thpr): the class was created as SpellReferenceChoiceSet and implements PrimitiveChoiceSet — it did not extend ChoiceSet. Its equals/hashCode compared its own set field and were correct and self-consistent, because there was no parent equality to conflict with. The override was the class's only equality logic.
  • 2008, commit 93caefe (thpr, ~5 months later, "ADD:SPELLCASTER token update"): the class was refactored to extend ChoiceSet<PCClass> (which already defined equals/hashCode on setName ^ pcs with a permissive instanceof ChoiceSet check) and switched to types/primitives fields, but the subclass override was kept with a strict instanceof SpellCasterChoiceSet check. That is the exact commit where the asymmetry was introduced, and it has been latent ever since.

The override is now both redundant and incorrect: the typePCS that drives types/primitives is the same object passed to super(...) as the parent's pcs, so the parent's setName + pcs comparison already captures that identity. Inheriting the parent equality (as the sibling ChoiceSet.AbilityChoiceSet does, cleaned up the same way in #7628) loses no discriminating power — it only restores the symmetry and hashCode contracts.

Vest added 2 commits June 25, 2026 22:13
Parent ChoiceSet.equals accepts any ChoiceSet<?> (via pattern matching on
the parent type) and compares setName + pcs. The subclass override
required the other side to be a SpellCasterChoiceSet, so the equality
relation was not symmetric:

  parentChoiceSet.equals(spellCaster) -> true (setName + pcs match)
  spellCaster.equals(parentChoiceSet) -> false (rejected by instanceof)

hashCode was likewise inconsistent: parent hashes setName + pcs, subclass
hashed types + primitives. Equal-by-parent objects could end up in
different buckets.

The subclass's types / primitives fields are not part of identity beyond
what the parent already captures via the typePCS passed to super(...), so
the right pattern (already used by the sibling ChoiceSet.AbilityChoiceSet
in a7cd6c1 / PCGen#7628) is to delete the override and inherit. The class
gets @SuppressFBWarnings(EQ_DOESNT_OVERRIDE_EQUALS) with the rationale.

Adds SpellCasterChoiceSetEqualsTest pinning the symmetry contract; the
test would have failed against the pre-fix override.
PlayerCharacterTrackingFacet.getPC documents (lines 40-47) that it may
return null in unit-test paths where no PC has been associated with the
CharID. SpringHelper.getBean also returns null when no bean is registered
for the requested type. Both nulls used to flow straight into
desc.getDescription(aPC, ...), producing an NPE downstream.

SpotBugs flags this as NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE at
DescriptionActor.java:71 (charStore loaded, then dereferenced at line 72
without a guard).

Returns an empty wrapped string for the no-PC case, matching the existing
'no benefits' early-out a few lines above.
@Vest Vest self-assigned this Jul 18, 2026
@Vest Vest added the bug label Jul 18, 2026
@Vest
Vest merged commit dac7d2f into PCGen:master Jul 22, 2026
7 checks passed
@Vest
Vest deleted the spotbugs-symmetry-and-null-pc branch July 22, 2026 09:53
@github-actions

Copy link
Copy Markdown

🚧 PCGen Code Coverage

Overall Project 25.84%
Files changed 0%

File Coverage
SpellCasterChoiceSet.java 50.24% 🍏
DescriptionActor.java 6.82% -19.32%

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant