Skip to content

Region/Type: add value-equals; DisplayLocation: suppress identity-equals#7638

Draft
Vest wants to merge 2 commits into
PCGen:masterfrom
Vest:spotbugs-comparable-equals
Draft

Region/Type: add value-equals; DisplayLocation: suppress identity-equals#7638
Vest wants to merge 2 commits into
PCGen:masterfrom
Vest:spotbugs-comparable-equals

Conversation

@Vest

@Vest Vest commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes 3 EQ_COMPARETO_USE_OBJECT_EQUALS and 1 SI_INSTANCE_BEFORE_FINALS_ASSIGNED findings from SpotBugs in pcgen.cdom.enumeration. All three classes (Region, Type, DisplayLocation) implement Comparable without overriding equals/hashCode. Two of them are used as collection keys in production code; the third is not. Fix per case.

Changes

Region.java (used as collection key)

  • Added value-based equals and hashCode matching the existing case-insensitive compareTo.
  • equals uses String.CASE_INSENSITIVE_ORDER.compare(...) == 0 (per PR IMPROPER_UNICODE: replace equalsIgnoreCase where safe, suppress where idiomatic #7626 convention) so the find-sec-bugs IMPROPER_UNICODE rule stays clean.
  • hashCode folds via toLowerCase(Locale.ROOT) for a stable, locale-independent hash consistent with equals.
  • Rearranged private static int ordinalCount = 0 above the NONE declaration to silence SI_INSTANCE_BEFORE_FINALS_ASSIGNED (JLS default-initialises int to 0 anyway; pure static-layout cleanup).

Why safe: Region is used as Optional<Region> key in BioSet.ageMap / BioSet.userMap (DoubleKeyMap/TripleKeyMapToList) and .equals() is called directly in RegionFacet.matchesRegion. The getConstant intern map already made two getConstant(name) calls return the same instance, so the new equals/hashCode strengthens but does not change behaviour for any existing call site.

Type.java (used as collection key)

  • Added value-based equals and hashCode matching the existing case-sensitive compareTo.
  • Type is used as HashSet<Type> / HashMap<Type, ...> in 8 production files.
  • Since TYPE_MAP is a CaseInsensitiveMap, two distinct Type instances with fieldNames differing only in case cannot exist, so case-sensitive equals correctly matches compareTo.

DisplayLocation.java (not used as collection key)

  • Suppress with method-level @SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUALS") on compareTo.
  • Justification: singleton-by-construction via getConstant intern map; no .equals() callers, not used as a collection key. Identity equality is correct here.

Verification

  • ./gradlew compileJava: BUILD SUCCESSFUL
  • Scoped tests (pcgen.cdom.enumeration.*, pcgen.cdom.facet.*, pcgen.cdom.facet.fact.*, pcgen.core.*): 2528 tests, 0 failures, 0 errors.
  • SpotBugs delta (XML report, total <BugInstance> rows):
    • Before: 77
    • After: 73
    • EQ_COMPARETO_USE_OBJECT_EQUALS: 3 -> 0 (Region, Type, DisplayLocation)
    • SI_INSTANCE_BEFORE_FINALS_ASSIGNED: 1 -> 0 (Region.NONE)
    • No new findings introduced.

Vest added 2 commits June 25, 2026 22:15
Fixes 3 EQ_COMPARETO_USE_OBJECT_EQUALS and 1
SI_INSTANCE_BEFORE_FINALS_ASSIGNED finding from SpotBugs.

Region: add value-based equals/hashCode matching the case-insensitive
compareTo. Region is used as a HashMap<Optional<Region>, ...> key in
BioSet (ageMap, userMap) and .equals() is called directly in
RegionFacet.matchesRegion. The intern map in getConstant() already made
two getConstant(name) calls return the same instance, so the new
equals/hashCode strengthens but does not change behaviour for any
existing call site. Use String.CASE_INSENSITIVE_ORDER.compare(...) == 0
in equals to satisfy the find-sec-bugs IMPROPER_UNICODE rule (PR PCGen#7626
convention); hashCode folds via toLowerCase(Locale.ROOT) for a stable,
locale-independent hash consistent with equals.

Region: also rearrange the private static int ordinalCount = 0 above
the NONE constant declaration to silence SI_INSTANCE_BEFORE_FINALS_ASSIGNED.
JLS default-initialises primitive int to 0 before any field initialiser
runs, so this is purely a static-layout cleanup.

Type: add value-based equals/hashCode matching the case-sensitive
compareTo. Type is used as HashSet<Type>/HashMap<Type, ...> across
8 production files. Since the intern map is CaseInsensitiveMap, two
distinct Type instances with fieldNames differing only in case cannot
exist, so case-sensitive equals matches compareTo and preserves
existing behaviour.

DisplayLocation: identity-equals is already correct (singleton-by-
construction via getConstant intern map, no .equals() callers, not used
as a collection key). Method-level @SuppressFBWarnings on compareTo
documents that explicitly.
The original commit replaced the stale 'no equals/hashCode needed because
TypeSafeConstant has a private constructor' comment with the actual
equals/hashCode methods, but didn't leave anything in its place. Replaces
the lost archival value with a short class-level Javadoc note explaining
the design intent: equals/hashCode match compareTo (Comparable contract)
and support use as a HashMap key (BioSet, RegionFacet, DataSet, GameMode,
etc.). Captures the *why*, not the *what*, so future readers don't have
to rediscover the contract reasoning from git history.
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