Skip to content

SpotBugs correctness: SpellCasterChoiceSet symmetry + DescriptionActor null PC#7637

Draft
Vest wants to merge 2 commits into
PCGen:masterfrom
Vest:spotbugs-symmetry-and-null-pc
Draft

SpotBugs correctness: SpellCasterChoiceSet symmetry + DescriptionActor null PC#7637
Vest wants to merge 2 commits into
PCGen:masterfrom
Vest:spotbugs-symmetry-and-null-pc

Conversation

@Vest

@Vest Vest commented Jun 25, 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

  • NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE: 9 -> 8 (DescriptionActor.java gone)
  • EQ_OVERRIDING_EQUALS_NOT_SYMMETRIC / EQ_DOESNT_OVERRIDE_EQUALS: unchanged (0)
  • Total bug instances: 76 -> 75
  • No new findings.

Tests

./gradlew :test --tests pcgen.cdom.* --tests pcgen.output.* -> 2356 tests, 0 failures, 0 errors.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant