Skip to content

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

Merged
Vest merged 2 commits into
PCGen:masterfrom
Vest:spotbugs-comparable-equals
Jul 22, 2026
Merged

Region/Type: add value-equals; DisplayLocation: suppress identity-equals#7638
Vest merged 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 July 22, 2026 14:00
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.
@Vest
Vest force-pushed the spotbugs-comparable-equals branch from 7a8bcf5 to 7052340 Compare July 22, 2026 12:04
@Vest
Vest marked this pull request as ready for review July 22, 2026 12:04
@Vest Vest self-assigned this Jul 22, 2026
@Vest Vest added the bug label Jul 22, 2026
@Vest
Vest merged commit 996ec96 into PCGen:master Jul 22, 2026
4 checks passed
@Vest
Vest deleted the spotbugs-comparable-equals branch July 22, 2026 12:25
@github-actions

Copy link
Copy Markdown

🚧 PCGen Code Coverage

Overall Project 25.85%
Files changed 86.67% 🍏

File Coverage
Type.java 89.58% 🍏
Region.java 72.05% -3.73%
DisplayLocation.java 68.92% 🍏

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