Skip to content

TypeSafeConstant siblings: use Map.computeIfAbsent in getConstant#7641

Open
Vest wants to merge 2 commits into
PCGen:masterfrom
Vest:typesafeconstant-computeifabsent
Open

TypeSafeConstant siblings: use Map.computeIfAbsent in getConstant#7641
Vest wants to merge 2 commits into
PCGen:masterfrom
Vest:typesafeconstant-computeifabsent

Conversation

@Vest

@Vest Vest commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

SonarLint java:S3824 cleanup. Three sibling TypeSafeConstant classes (Type, Region, DisplayLocation) had identical get/null-check/put bodies in their getConstant factories:

// before
public static Type getConstant(String name) {
    Type type = TYPE_MAP.get(name);
    if (type == null) {
        type = new Type(name);
        TYPE_MAP.put(name, type);
    }
    return type;
}

// after
public static Type getConstant(String name) {
    return TYPE_MAP.computeIfAbsent(name, k -> new Type(name));
}

Same for Region.getConstant (preserving the leading initializeTypeMap() call) and DisplayLocation.getConstant.

Why the lambda captures name, not k

CaseInsensitiveMap.computeIfAbsent overrides the JDK default to pass resolveObject(key) — a fresh CaseInsensitiveString(key) wrapper — into the mapping function. The function's parameter type is therefore ? super Object, not ? super String. A Type::new method reference would not satisfy that signature.

The lambda captures the outer name parameter (a String) directly, which is exactly what each constructor expects. Behaviour is identical to the prior code: CaseInsensitiveString.toString() returns its underlying string, so even if the lambda used k.toString() it would produce the same value.

What's NOT changed

The sibling lookup-or-throw methods in Region.valueOf and DisplayLocation.valueOf keep their get(name); if null throw shape — that's the intentionally-different "fail-on-missing" idiom, not a candidate for computeIfAbsent.

Verification

  • ./gradlew compileJava — BUILD SUCCESSFUL
  • ./gradlew :test --tests "pcgen.cdom.enumeration.*" --tests "pcgen.cdom.facet.*" --tests "pcgen.core.*" --tests "plugin.lsttokens.*"12 241 tests, 0 failures
  • SpotBugs total unchanged at 77 (this is not a SpotBugs PR; the check is for regression sanity)

Diff size

3 files changed, +3 / −21 lines.

Scope note

Independent of all the in-flight SpotBugs PRs (#7631#7639). Sits on top of fresh origin/master.

Collapses the three get/null-check/put bodies in Type.getConstant,
Region.getConstant, and DisplayLocation.getConstant into a single
computeIfAbsent call. Behaviour-identical and one map traversal
instead of two. SonarLint java:S3824.

The lambda captures `name` from the enclosing scope rather than the
map-key parameter `k`, because CaseInsensitiveMap.computeIfAbsent
passes a CaseInsensitiveString wrapper (via resolveObject) rather than
the original String; a `Type::new` method reference would not satisfy
the Function<? super Object, ? extends Type> signature. The behaviour
is identical to the prior code since CaseInsensitiveString.toString()
returns the original string anyway.

Does NOT touch the sibling "lookup-or-throw" methods
(Region.valueOf, DisplayLocation.valueOf) - those have the
intentionally-different shape `get(name); if null throw`.

Verified: 12241 tests across pcgen.cdom.enumeration.*, pcgen.cdom.facet.*,
pcgen.core.*, plugin.lsttokens.* pass with 0 failures. SpotBugs total
unchanged at 77 (this is not a SpotBugs PR; the check is for regression
sanity).
@Vest Vest marked this pull request as ready for review June 26, 2026 10:33
The three getConstant factories rely on a singleton-by-name intern
contract: getConstant("X") and getConstant("X") must return the same
instance, getConstant("X") and getConstant("x") must return the same
instance (CaseInsensitiveMap-backed), and getConstant("X") /
getConstant("Y") must return distinct instances. This contract was
load-bearing across the codebase (Type as HashSet/HashMap key in
DataSet/GameMode/etc., Region as HashMap key in BioSet) but had zero
direct test coverage until now:

  - TypeTest had one incidental test that didn't assert intern identity.
  - RegionTest and DisplayLocationTest didn't exist.

Adds:
  - TypeTest: getConstantInternsByName, getConstantInternsCaseInsensitively,
    getConstantDistinguishesNames.
  - RegionTest (new file): same three.
  - DisplayLocationTest (new file): same three, plus two for valueOf
    (returns interned instance; throws on unknown name).

Fixture names are namespaced ("TypeTest_*", "RegionTest_*", etc.) so
they don't collide with data-load fixtures in other tests sharing the
JVM. None of the three classes has a per-test reset hook on master
(Type has no clearConstants at all; Region/DisplayLocation have one
but no test currently calls it).

Revert-and-rerun verified: temporarily replacing Type.getConstant with
"return new Type(name);" caused getConstantInternsByName and
getConstantInternsCaseInsensitively to fail (as designed), while the
pre-existing testSortable kept passing -- confirming the pre-existing
test was not exercising the intern invariant.
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