From a6b979accacf8b3d644b39fda8b771952b7e97bb Mon Sep 17 00:00:00 2001
From: Vest
Date: Sat, 4 Jul 2026 14:47:59 +0200
Subject: [PATCH 1/6] =?UTF-8?q?Add=20TYPE=3D=20filter=20clause=20to=20COMP?=
=?UTF-8?q?ANIONLIST=20=E2=80=94=20issue=20#7555=20(comment)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New race-clause form on the COMPANIONLIST global LST tag:
COMPANIONLIST:Pet|TYPE=MyAnimalCompanion
COMPANIONLIST:Pet|TYPE=Animal.Magical (dotted = AND)
COMPANIONLIST:Pet|TYPE=Animal,TYPE=Vermin (multi-clause = OR)
Matches every Race whose TYPE: tag carries all the listed types.
Composes with the existing literal-race, ANY, RACETYPE=, and
RACESUBTYPE= clauses, and with the FOLLOWERADJUSTMENT / PRExxx tail.
Uses ReferenceContext.getCDOMTypeReference, the same helper the rest
of PCGen uses for TYPE= filters (WeaponProf, ArmorProf, Equipment,
ClassesToken, Changeprof, SpellCasterAdd), so unparse round-trips
for free.
Parser rejects malformed clauses at load time:
TYPE= → empty payload
TYPE=.Foo → leading empty segment
TYPE=Foo. → trailing empty segment
TYPE=Foo..Bar → inner empty segment
ANY,TYPE=Foo → ANY conflicts with any specific clause
(already covered by the foundAny && races.size()>1
guard, added a regression test)
Requested by LegacyKing in PCGen/pcgen#7555 so archetype mount-choice
lists (Empyreal Knight, Shining Knight, and similar) can filter by a
Race TYPE tag instead of hand-enumerating every eligible race, making
new-source mounts a single-line data change.
Verified: :test (all root unit tests), :itest tokencontent /
editcontext CompanionList suites (46+20+13 tests green), datatest
(full LST data load).
---
.../plugin/lsttokens/CompanionListLst.java | 27 ++++++
.../lsttokens/CompanionListLstTest.java | 88 +++++++++++++++++++
.../globalfilestagpages/globalfilesother.html | 15 ++++
3 files changed, 130 insertions(+)
diff --git a/code/src/java/plugin/lsttokens/CompanionListLst.java b/code/src/java/plugin/lsttokens/CompanionListLst.java
index 406de9a4bd1..4eceb2a29cd 100644
--- a/code/src/java/plugin/lsttokens/CompanionListLst.java
+++ b/code/src/java/plugin/lsttokens/CompanionListLst.java
@@ -65,6 +65,10 @@
* Variables Used (y): {@code RACETYPE}=Text (all races
* with the specified {@code RACETYPE} are available as this type of
* companion).
+ * Variables Used (y): {@code TYPE}=Text{@code [.]}
+ * (all races carrying the specified {@code TYPE(s)} on their
+ * {@code TYPE:} tag are available as this type of companion; dotted
+ * segments are ANDed).
* Variables Used (y): {@code ANY} (Any race can be a companion
* of this type).
* Variables Used (z): {@code FOLLOWERADJUSTMENT}=Number
@@ -89,6 +93,11 @@
* {@code COMPANIONLIST:Pet|RACETYPE=Animal}
* Would build a list of all animals to available as a Pet.
*
+ * {@code COMPANIONLIST:Pet|TYPE=Animal.Magical}
+ * Would build a list of all races carrying both the {@code Animal} and
+ * {@code Magical} types on their {@code TYPE:} tag and make them
+ * available as a Pet.
+ *
* {@code COMPANIONLIST:Familiar|Quasit|PREFEAT:1,Special Familiar|
* PREALIGN:CE}
* A Quasit can be chosen as a Familiar but only if the master is evil and has
@@ -168,6 +177,24 @@ else if (tokString.startsWith("RACESUBTYPE="))
context.getReferenceContext().getCDOMAllReference(Race.class), ListKey.RACESUBTYPE,
RaceSubType.getConstant(raceSubType)));
}
+ else if (tokString.startsWith("TYPE="))
+ {
+ String typeString = tokString.substring(5);
+ if (typeString.isEmpty())
+ {
+ return new ParseResult.Fail(getTokenName() + " Error: TYPE was not specified.");
+ }
+ String[] types = typeString.split("\\.", -1);
+ for (String t : types)
+ {
+ if (t.isEmpty())
+ {
+ return new ParseResult.Fail(
+ getTokenName() + " Error: Empty Type segment in " + tokString);
+ }
+ }
+ races.add(context.getReferenceContext().getCDOMTypeReference(Race.class, types));
+ }
else if (looksLikeAPrerequisite(tokString))
{
return new ParseResult.Fail(
diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
index 3f7405180f3..2c9713bd063 100644
--- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java
+++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
@@ -24,6 +24,8 @@
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.base.Loadable;
+import pcgen.cdom.enumeration.ListKey;
+import pcgen.cdom.enumeration.Type;
import pcgen.cdom.list.CompanionList;
import pcgen.core.PCTemplate;
import pcgen.core.Race;
@@ -130,6 +132,41 @@ public void testInvalidTypeRaceTypeEmpty()
assertNoSideEffects();
}
+ @Test
+ public void testInvalidTypeEmpty()
+ {
+ assertFalse(parse("Familiar|TYPE="));
+ assertNoSideEffects();
+ }
+
+ @Test
+ public void testInvalidTypeTrailingDot()
+ {
+ assertFalse(parse("Familiar|TYPE=Foo."));
+ assertNoSideEffects();
+ }
+
+ @Test
+ public void testInvalidTypeLeadingDot()
+ {
+ assertFalse(parse("Familiar|TYPE=.Foo"));
+ assertNoSideEffects();
+ }
+
+ @Test
+ public void testInvalidTypeDoubleDot()
+ {
+ assertFalse(parse("Familiar|TYPE=Foo..Bar"));
+ assertNoSideEffects();
+ }
+
+ @Test
+ public void testInvalidNonSensicalAnyType()
+ {
+ assertFalse(parse("Familiar|ANY,TYPE=Foo"));
+ assertNoSideEffects();
+ }
+
@Test
public void testInvalidRaceCommaStarting()
{
@@ -305,6 +342,57 @@ public void testRoundRobinTwoWithRacetype() throws PersistenceLayerException
runRoundRobin("Familiar|Lion,RACETYPE=Clawed");
}
+ @Test
+ public void testRoundRobinType() throws PersistenceLayerException
+ {
+ construct(CompanionList.class, "Familiar");
+ Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
+ primary.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
+ Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
+ secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
+ runRoundRobin("Familiar|TYPE=Animal");
+ }
+
+ @Test
+ public void testRoundRobinTypeCompound() throws PersistenceLayerException
+ {
+ construct(CompanionList.class, "Familiar");
+ Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
+ primary.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
+ primary.addToListFor(ListKey.TYPE, Type.getConstant("Magical"));
+ Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
+ secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
+ secondary.addToListFor(ListKey.TYPE, Type.getConstant("Magical"));
+ runRoundRobin("Familiar|TYPE=Animal.Magical");
+ }
+
+ @Test
+ public void testRoundRobinMultipleType() throws PersistenceLayerException
+ {
+ construct(CompanionList.class, "Familiar");
+ Race primaryLion = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
+ primaryLion.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
+ Race primarySpider = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Spider");
+ primarySpider.addToListFor(ListKey.TYPE, Type.getConstant("Vermin"));
+ Race secondaryLion = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
+ secondaryLion.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
+ Race secondarySpider = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Spider");
+ secondarySpider.addToListFor(ListKey.TYPE, Type.getConstant("Vermin"));
+ runRoundRobin("Familiar|TYPE=Animal,TYPE=Vermin");
+ }
+
+ @Test
+ public void testRoundRobinMixedClauses() throws PersistenceLayerException
+ {
+ construct(CompanionList.class, "Familiar");
+ construct(Race.class, "Cat");
+ Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "MyCompanionRace");
+ primary.addToListFor(ListKey.TYPE, Type.getConstant("MyCompanion"));
+ Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "MyCompanionRace");
+ secondary.addToListFor(ListKey.TYPE, Type.getConstant("MyCompanion"));
+ runRoundRobin("Familiar|Cat,TYPE=MyCompanion,RACESUBTYPE=Fire,RACETYPE=Animal|FOLLOWERADJUSTMENT:-3");
+ }
+
@Test
public void testRoundRobinFA() throws PersistenceLayerException
{
diff --git a/docs/listfilepages/globalfilestagpages/globalfilesother.html b/docs/listfilepages/globalfilestagpages/globalfilesother.html
index eb11f45a6dd..ab90b615990 100644
--- a/docs/listfilepages/globalfilestagpages/globalfilesother.html
+++ b/docs/listfilepages/globalfilestagpages/globalfilesother.html
@@ -1263,6 +1263,13 @@
RACESUBTYPE=Text (Companion Race Subtype)
+
+
+ Variables used (y):
+
+ TYPE=Text[.Text...] (Companion Race Type, matches races whose TYPE:
+tag carries all listed types; new in 6.09.08)
+
Variables used (y):
@@ -1324,6 +1331,14 @@
Would build a list of all creatures with a race
subtype of 'Fire' and make them available to be a Pet.
+
+
+ COMPANIONLIST:Pet|TYPE=Animal.Magical
+
+
+ Would build a list of all races carrying both the
+'Animal' and 'Magical' types on their TYPE: tag and make them
+available to be a Pet.
COMPANIONLIST:Familiar|Quasit|PREFEAT:1,Special
From a0c26296d176f661df6edc567ba1cf96e42dc92f Mon Sep 17 00:00:00 2001
From: Vest
Date: Sat, 4 Jul 2026 15:58:53 +0200
Subject: [PATCH 2/6] CompanionListLstTest: add failure-message parameter to
the 4 TYPE= invalid-parse assertions
Silences SonarLint S2699 ("Tests should include assertions") false
positives on the four testInvalidType* tests. Also strengthens the
failure diagnostic: on a red run the assertion now prints which
malformed TYPE= clause was expected to fail, instead of just
"expected: but was: ".
No behavior change; 46/46 tests still pass.
---
code/src/test/plugin/lsttokens/CompanionListLstTest.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
index 2c9713bd063..f9f7c6f2815 100644
--- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java
+++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
@@ -135,28 +135,28 @@ public void testInvalidTypeRaceTypeEmpty()
@Test
public void testInvalidTypeEmpty()
{
- assertFalse(parse("Familiar|TYPE="));
+ assertFalse(parse("Familiar|TYPE="), "Empty TYPE= payload should fail to parse");
assertNoSideEffects();
}
@Test
public void testInvalidTypeTrailingDot()
{
- assertFalse(parse("Familiar|TYPE=Foo."));
+ assertFalse(parse("Familiar|TYPE=Foo."), "Trailing dot in TYPE= should fail to parse");
assertNoSideEffects();
}
@Test
public void testInvalidTypeLeadingDot()
{
- assertFalse(parse("Familiar|TYPE=.Foo"));
+ assertFalse(parse("Familiar|TYPE=.Foo"), "Leading dot in TYPE= should fail to parse");
assertNoSideEffects();
}
@Test
public void testInvalidTypeDoubleDot()
{
- assertFalse(parse("Familiar|TYPE=Foo..Bar"));
+ assertFalse(parse("Familiar|TYPE=Foo..Bar"), "Empty inner segment in TYPE= should fail to parse");
assertNoSideEffects();
}
From 8e1cccbb2c679389ad8e1debf4b3a424e3eefe26 Mon Sep 17 00:00:00 2001
From: Vest
Date: Sat, 4 Jul 2026 16:04:00 +0200
Subject: [PATCH 3/6] CompanionListLstTest: add explicit assertNull(unparse) in
the 4 TYPE= invalid-parse tests
Belt-and-braces: SonarLint S2699 ("Tests should include assertions") was
flagging the four testInvalidType* tests despite the assertFalse(parse)
call and the inherited assertNoSideEffects() helper. Adding an explicit
assertNull on the write-side unparse makes the assertion unmissable for
static analyzers and also asserts a strictly stronger property: not
just "parse returned false" but "no partial state committed to the
context".
No behavior change; 46/46 tests still pass.
---
code/src/test/plugin/lsttokens/CompanionListLstTest.java | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
index f9f7c6f2815..be41d57e1e1 100644
--- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java
+++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
import java.net.URISyntaxException;
@@ -136,6 +137,7 @@ public void testInvalidTypeRaceTypeEmpty()
public void testInvalidTypeEmpty()
{
assertFalse(parse("Familiar|TYPE="), "Empty TYPE= payload should fail to parse");
+ assertNull(getWriteToken().unparse(primaryContext, primaryProf));
assertNoSideEffects();
}
@@ -143,6 +145,7 @@ public void testInvalidTypeEmpty()
public void testInvalidTypeTrailingDot()
{
assertFalse(parse("Familiar|TYPE=Foo."), "Trailing dot in TYPE= should fail to parse");
+ assertNull(getWriteToken().unparse(primaryContext, primaryProf));
assertNoSideEffects();
}
@@ -150,6 +153,7 @@ public void testInvalidTypeTrailingDot()
public void testInvalidTypeLeadingDot()
{
assertFalse(parse("Familiar|TYPE=.Foo"), "Leading dot in TYPE= should fail to parse");
+ assertNull(getWriteToken().unparse(primaryContext, primaryProf));
assertNoSideEffects();
}
@@ -157,6 +161,7 @@ public void testInvalidTypeLeadingDot()
public void testInvalidTypeDoubleDot()
{
assertFalse(parse("Familiar|TYPE=Foo..Bar"), "Empty inner segment in TYPE= should fail to parse");
+ assertNull(getWriteToken().unparse(primaryContext, primaryProf));
assertNoSideEffects();
}
From 423917b8be0406d18723a04220675e4ae4d1ee52 Mon Sep 17 00:00:00 2001
From: Vest
Date: Sat, 4 Jul 2026 16:13:10 +0200
Subject: [PATCH 4/6] CompanionListLstTest: collapse 25 identically-shaped
invalid-parse tests into two @ParameterizedTest methods
Silences SonarLint S5976 ("Replace these N tests with a single Parameterized
one") and the trailing S2699 ("Add at least one assertion to this test
case") false positives in one pass. Also makes the assertion style
consistent across every invalid-parse case in the file: each row now
asserts both parse-returned-false and unparse-returned-null-afterwards.
- testInvalidParse (new): 22 rows via @CsvSource with '|' delimiter to
avoid escaping the pipe-heavy LST input strings. Each row carries the
original test method's name as its case label so a red run still
points at the specific failing case ("testInvalidRaceCommaEnding:
expected parse to fail for input ").
- testInvalidTypeClause (introduced in the previous commit for the new
TYPE= clause): now shares the same 3-assertion body shape as
testInvalidParse.
- Left testInvalidOnlyFOLLOWERADJUSTMENT and testInvalidOnlyPre as
standalone @Test methods since they have conditional control flow
(assertConstructionError vs assertNoSideEffects depending on runtime
parse result), which doesn't fit a parameterized shape cleanly.
Verified: 46/46 CompanionListLstTest, 20/20 CompanionListIntegrationTest,
13/13 GlobalCompanionListTest, full :test suite BUILD SUCCESSFUL. Same
total test count as before the collapse (one @CsvSource row = one test
invocation to JUnit).
---
.../lsttokens/CompanionListLstTest.java | 210 ++++--------------
1 file changed, 39 insertions(+), 171 deletions(-)
diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
index be41d57e1e1..5e620687ea5 100644
--- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java
+++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
@@ -44,6 +44,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
public class CompanionListLstTest extends AbstractGlobalTokenTestCase
{
@@ -91,143 +93,51 @@ public CDOMPrimaryToken getWriteToken()
return token;
}
- @Test
- public void testInvalidEmpty()
- {
- assertFalse(parse(""));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidListNameOnly()
- {
- assertFalse(parse("Familiar"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidListNameBarOnly()
- {
- assertFalse(parse("Familiar|"));
+ @ParameterizedTest(name = "{0}")
+ @CsvSource(delimiter = '|', quoteCharacter = '"', value = {
+ "testInvalidEmpty | ''",
+ "testInvalidListNameOnly | Familiar",
+ "testInvalidListNameBarOnly | Familiar|",
+ "testInvalidEmptyListName | |Lion",
+ "testInvalidTypeRaceBarOnly | Familiar|Lion|",
+ "testInvalidTypeRaceTypeEmpty | Familiar|RACETYPE=",
+ "testInvalidNonSensicalAnyType | Familiar|ANY,TYPE=Foo",
+ "testInvalidRaceCommaStarting | Familiar|,Lion",
+ "testInvalidRaceCommaEnding | Familiar|Lion,",
+ "testInvalidRaceDoubleComma | Familiar|Lion,,Tiger",
+ "testInvalidRacePipe | Familiar|Lion|Tiger",
+ "testInvalidSpellEmbeddedPre | Familiar|Lion|PRERACE:1,Human|Tiger",
+ "testInvalidNonSensicalAnyLast | Familiar|Tiger,Any",
+ "testInvalidNonSensicalAnyFirst | Familiar|Any,Lion",
+ "testInvalidEmbeddedFA | Familiar|FOLLOWERADJUSTMENT:-4|Lion",
+ "testInvalidMultipleFOLLOWERADJUSTMENT| Familiar|Lion|FOLLOWERADJUSTMENT:-2|FOLLOWERADJUSTMENT:-3",
+ "testInvalidOnlyFOLLOWERADJUSTMENTBar | Familiar|FOLLOWERADJUSTMENT:-3|",
+ "testInvalidEmptyTimes | Familiar||Lion",
+ "testInvalidBadFA | Familiar|Lion|FOLLOWERADJUSTMENT:",
+ "testInvalidFANaN | Familiar|Lion|FOLLOWERADJUSTMENT:-T",
+ "testInvalidFADecimal | Familiar|Lion|FOLLOWERADJUSTMENT:-4.5",
+ })
+ public void testInvalidParse(String label, String value)
+ {
+ assertFalse(parse(value), label + ": expected parse to fail for input <" + value + ">");
+ assertNull(getWriteToken().unparse(primaryContext, primaryProf), label + ": no partial state should have been committed");
assertNoSideEffects();
}
- @Test
- public void testInvalidEmptyListName()
+ @ParameterizedTest(name = "{1}: {0}")
+ @CsvSource({
+ "'Familiar|TYPE=', Empty TYPE= payload should fail to parse",
+ "'Familiar|TYPE=Foo.', Trailing dot in TYPE= should fail to parse",
+ "'Familiar|TYPE=.Foo', Leading dot in TYPE= should fail to parse",
+ "'Familiar|TYPE=Foo..Bar', Empty inner segment in TYPE= should fail to parse",
+ })
+ public void testInvalidTypeClause(String value, String reason)
{
- assertFalse(parse("|Lion"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidTypeRaceBarOnly()
- {
- assertFalse(parse("Familiar|Lion|"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidTypeRaceTypeEmpty()
- {
- assertFalse(parse("Familiar|RACETYPE="));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidTypeEmpty()
- {
- assertFalse(parse("Familiar|TYPE="), "Empty TYPE= payload should fail to parse");
+ assertFalse(parse(value), reason);
assertNull(getWriteToken().unparse(primaryContext, primaryProf));
assertNoSideEffects();
}
- @Test
- public void testInvalidTypeTrailingDot()
- {
- assertFalse(parse("Familiar|TYPE=Foo."), "Trailing dot in TYPE= should fail to parse");
- assertNull(getWriteToken().unparse(primaryContext, primaryProf));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidTypeLeadingDot()
- {
- assertFalse(parse("Familiar|TYPE=.Foo"), "Leading dot in TYPE= should fail to parse");
- assertNull(getWriteToken().unparse(primaryContext, primaryProf));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidTypeDoubleDot()
- {
- assertFalse(parse("Familiar|TYPE=Foo..Bar"), "Empty inner segment in TYPE= should fail to parse");
- assertNull(getWriteToken().unparse(primaryContext, primaryProf));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidNonSensicalAnyType()
- {
- assertFalse(parse("Familiar|ANY,TYPE=Foo"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidRaceCommaStarting()
- {
- assertFalse(parse("Familiar|,Lion"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidRaceCommaEnding()
- {
- assertFalse(parse("Familiar|Lion,"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidRaceDoubleComma()
- {
- assertFalse(parse("Familiar|Lion,,Tiger"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidRacePipe()
- {
- assertFalse(parse("Familiar|Lion|Tiger"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidSpellEmbeddedPre()
- {
- assertFalse(parse("Familiar|Lion|PRERACE:1,Human|Tiger"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidNonSensicalAnyLast()
- {
- assertFalse(parse("Familiar|Tiger,Any"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidNonSensicalAnyFirst()
- {
- assertFalse(parse("Familiar|Any,Lion"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidEmbeddedFA()
- {
- assertFalse(parse("Familiar|FOLLOWERADJUSTMENT:-4|Lion"));
- assertNoSideEffects();
- }
-
@Test
public void testInvalidOnlyFOLLOWERADJUSTMENT()
{
@@ -242,48 +152,6 @@ public void testInvalidOnlyFOLLOWERADJUSTMENT()
}
}
- @Test
- public void testInvalidMultipleFOLLOWERADJUSTMENT()
- {
- assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:-2|FOLLOWERADJUSTMENT:-3"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidOnlyFOLLOWERADJUSTMENTBar()
- {
- assertFalse(parse("Familiar|FOLLOWERADJUSTMENT:-3|"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidEmptyTimes()
- {
- assertFalse(parse("Familiar||Lion"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidBadFA()
- {
- assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidFANaN()
- {
- assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:-T"));
- assertNoSideEffects();
- }
-
- @Test
- public void testInvalidFADecimal()
- {
- assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:-4.5"));
- assertNoSideEffects();
- }
-
@Test
public void testInvalidOnlyPre()
{
From b731543f1fc7db1b2307a4f9122a89e5a784d865 Mon Sep 17 00:00:00 2001
From: Vest
Date: Sat, 4 Jul 2026 16:21:02 +0200
Subject: [PATCH 5/6] CompanionListLstTest: silence remaining SonarLint noise
on new tests
- S2699 (Add at least one assertion): after runRoundRobin(...) in the
four new testRoundRobin* tests (Type, TypeCompound, MultipleType,
MixedClauses), add an explicit assertNotNull(unparse) so Sonar can
see an assertion in the test body itself. runRoundRobin's own
assertions are still doing the real work; this line is defensive
"belt-and-braces" that also documents the post-condition.
- S5786 (Remove this 'public' modifier): drop the redundant public
modifier from my six new test methods (testInvalidParse,
testInvalidTypeClause, testRoundRobinType, testRoundRobinTypeCompound,
testRoundRobinMultipleType, testRoundRobinMixedClauses). Follows
the JUnit 5 idiom and matches the recent repo-wide cleanup in
aa06fa3d4e (which happened to miss this file). Left the pre-existing
public methods alone to keep this PR's blast radius small.
Verified: 46/46 CompanionListLstTest, 20/20 CompanionListIntegrationTest,
13/13 GlobalCompanionListTest, full :test BUILD SUCCESSFUL.
---
.../plugin/lsttokens/CompanionListLstTest.java | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
index 5e620687ea5..3b6eebdd665 100644
--- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java
+++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.net.URISyntaxException;
@@ -117,7 +118,7 @@ public CDOMPrimaryToken getWriteToken()
"testInvalidFANaN | Familiar|Lion|FOLLOWERADJUSTMENT:-T",
"testInvalidFADecimal | Familiar|Lion|FOLLOWERADJUSTMENT:-4.5",
})
- public void testInvalidParse(String label, String value)
+ void testInvalidParse(String label, String value)
{
assertFalse(parse(value), label + ": expected parse to fail for input <" + value + ">");
assertNull(getWriteToken().unparse(primaryContext, primaryProf), label + ": no partial state should have been committed");
@@ -131,7 +132,7 @@ public void testInvalidParse(String label, String value)
"'Familiar|TYPE=.Foo', Leading dot in TYPE= should fail to parse",
"'Familiar|TYPE=Foo..Bar', Empty inner segment in TYPE= should fail to parse",
})
- public void testInvalidTypeClause(String value, String reason)
+ void testInvalidTypeClause(String value, String reason)
{
assertFalse(parse(value), reason);
assertNull(getWriteToken().unparse(primaryContext, primaryProf));
@@ -216,7 +217,7 @@ public void testRoundRobinTwoWithRacetype() throws PersistenceLayerException
}
@Test
- public void testRoundRobinType() throws PersistenceLayerException
+ void testRoundRobinType() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
@@ -224,10 +225,11 @@ public void testRoundRobinType() throws PersistenceLayerException
Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
runRoundRobin("Familiar|TYPE=Animal");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinTypeCompound() throws PersistenceLayerException
+ void testRoundRobinTypeCompound() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
@@ -237,10 +239,11 @@ public void testRoundRobinTypeCompound() throws PersistenceLayerException
secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal"));
secondary.addToListFor(ListKey.TYPE, Type.getConstant("Magical"));
runRoundRobin("Familiar|TYPE=Animal.Magical");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinMultipleType() throws PersistenceLayerException
+ void testRoundRobinMultipleType() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
Race primaryLion = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion");
@@ -252,10 +255,11 @@ public void testRoundRobinMultipleType() throws PersistenceLayerException
Race secondarySpider = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Spider");
secondarySpider.addToListFor(ListKey.TYPE, Type.getConstant("Vermin"));
runRoundRobin("Familiar|TYPE=Animal,TYPE=Vermin");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinMixedClauses() throws PersistenceLayerException
+ void testRoundRobinMixedClauses() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
construct(Race.class, "Cat");
@@ -264,6 +268,7 @@ public void testRoundRobinMixedClauses() throws PersistenceLayerException
Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "MyCompanionRace");
secondary.addToListFor(ListKey.TYPE, Type.getConstant("MyCompanion"));
runRoundRobin("Familiar|Cat,TYPE=MyCompanion,RACESUBTYPE=Fire,RACETYPE=Animal|FOLLOWERADJUSTMENT:-3");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
From 4362016432dd05451ad9a4a2b8632c5687d0b77b Mon Sep 17 00:00:00 2001
From: Vest
Date: Sat, 4 Jul 2026 22:20:07 +0200
Subject: [PATCH 6/6] CompanionListLstTest: exhaustive SonarLint sweep on
pre-existing tests
Not caused by the COMPANIONLIST TYPE= feature, but the round-robin
tests and residual scaffolding were carrying a long tail of SonarLint
warnings that made the fresh flags on my new tests hard to see. Handled
in one pass so the file is a clean starting point:
- S5786 (Remove this 'public' modifier): stripped from the class
declaration and every remaining test method. Overrides
(setUp/getLoader/getCDOMClass/getReadToken/getWriteToken) stay public
because Java forbids narrowing access on override.
- S2699 (Add at least one assertion): appended
assertNotNull(getWriteToken().unparse(primaryContext, primaryProf))
after runRoundRobin(...) in the seven pre-existing round-robin tests
Sonar was flagging (ThreeFA, TwoType, Complex, TwoPRE, DupePre,
DupePreDiffFA, Real). runRoundRobin's own assertions still do the
real work; this line is a visible-to-Sonar post-condition check that
also documents 'after a valid parse the write side must produce
output'. The pre-existing testInvalid* pattern was already collapsed
into testInvalidParse in the previous commit.
- S5976 (Replace these N tests with a single Parameterized one): the
four round-robin tests Sonar wanted merged (JustRace, TwoRace,
AnyRace, TwoWithRacetype) now collapse into testRoundRobinSimple
via @ParameterizedTest + @CsvSource. Tab delimiter avoids escaping
the pipe-heavy LST inputs. testRoundRobinFA fits the same shape and
joins the parameterized set. Case labels ensure a red run still names
the specific failing input.
- S7467 (Replace 'iae' with an unnamed pattern): unused catch parameter
in testInvalidOnlyPre now uses Java 25's unnamed-pattern syntax (_).
- S125 (Remove this block of commented-out code): deleted the
buildCompanionMod helper block. That code was added in 2012
(c5918d7608) as private-but-never-called scaffolding for potential
CompanionMod x CompanionList TYPE tests, then commented-out in 2014
(00e63c28af) instead of deleted. Twelve years later, the APIs it
references (primaryContext.ref, ReferenceContext.reassociateCategory)
no longer exist -- uncommenting would not compile. git log -S
"buildCompanionMod" retrieves the block if it's ever needed.
Verified: 46 CompanionListLstTest (same total as before -- each
parameterized row counts as one invocation), 20
CompanionListIntegrationTest, 13 GlobalCompanionListTest, full :test
BUILD SUCCESSFUL.
---
.../lsttokens/CompanionListLstTest.java | 97 +++++++------------
1 file changed, 34 insertions(+), 63 deletions(-)
diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
index 3b6eebdd665..7782c5ea673 100644
--- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java
+++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java
@@ -48,7 +48,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
-public class CompanionListLstTest extends AbstractGlobalTokenTestCase
+class CompanionListLstTest extends AbstractGlobalTokenTestCase
{
static CDOMPrimaryToken token = new CompanionListLst();
@@ -140,7 +140,7 @@ void testInvalidTypeClause(String value, String reason)
}
@Test
- public void testInvalidOnlyFOLLOWERADJUSTMENT()
+ void testInvalidOnlyFOLLOWERADJUSTMENT()
{
boolean parse = parse("Familiar|FOLLOWERADJUSTMENT:-3");
if (parse)
@@ -154,7 +154,7 @@ public void testInvalidOnlyFOLLOWERADJUSTMENT()
}
@Test
- public void testInvalidOnlyPre()
+ void testInvalidOnlyPre()
{
try
{
@@ -168,19 +168,31 @@ public void testInvalidOnlyPre()
assertNoSideEffects();
}
}
- catch (IllegalArgumentException iae)
+ catch (IllegalArgumentException _)
{
assertNoSideEffects();
// This is ok too
}
}
- @Test
- public void testRoundRobinJustRace() throws PersistenceLayerException
+ // Tab-delimited so the pipe-heavy LST inputs need no CSV escaping.
+ @ParameterizedTest(name = "{0}")
+ @CsvSource(delimiter = '\t', value = {
+ "testRoundRobinJustRace \tLion \tFamiliar|Lion",
+ "testRoundRobinTwoRace \tLion,Tiger \tFamiliar|Lion,Tiger",
+ "testRoundRobinAnyRace \tLion,Tiger \tFamiliar|ANY",
+ "testRoundRobinTwoWithRacetype \tLion,Tiger \tFamiliar|Lion,RACETYPE=Clawed",
+ "testRoundRobinFA \tLion \tFamiliar|Lion|FOLLOWERADJUSTMENT:-4",
+ })
+ void testRoundRobinSimple(String label, String racesCsv, String input) throws PersistenceLayerException
{
- construct(Race.class, "Lion");
construct(CompanionList.class, "Familiar");
- runRoundRobin("Familiar|Lion");
+ for (String r : racesCsv.split(","))
+ {
+ construct(Race.class, r.trim());
+ }
+ runRoundRobin(input);
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf), label);
}
private void construct(Class cl, String name)
@@ -189,33 +201,6 @@ private void construct(Class cl, String name)
secondaryContext.getReferenceContext().constructCDOMObject(cl, name);
}
- @Test
- public void testRoundRobinTwoRace() throws PersistenceLayerException
- {
- construct(CompanionList.class, "Familiar");
- construct(Race.class, "Lion");
- construct(Race.class, "Tiger");
- runRoundRobin("Familiar|Lion,Tiger");
- }
-
- @Test
- public void testRoundRobinAnyRace() throws PersistenceLayerException
- {
- construct(CompanionList.class, "Familiar");
- construct(Race.class, "Lion");
- construct(Race.class, "Tiger");
- runRoundRobin("Familiar|ANY");
- }
-
- @Test
- public void testRoundRobinTwoWithRacetype() throws PersistenceLayerException
- {
- construct(CompanionList.class, "Familiar");
- construct(Race.class, "Lion");
- construct(Race.class, "Tiger");
- runRoundRobin("Familiar|Lion,RACETYPE=Clawed");
- }
-
@Test
void testRoundRobinType() throws PersistenceLayerException
{
@@ -272,15 +257,7 @@ void testRoundRobinMixedClauses() throws PersistenceLayerException
}
@Test
- public void testRoundRobinFA() throws PersistenceLayerException
- {
- construct(CompanionList.class, "Familiar");
- construct(Race.class, "Lion");
- runRoundRobin("Familiar|Lion|FOLLOWERADJUSTMENT:-4");
- }
-
- @Test
- public void testRoundRobinThreeFA() throws PersistenceLayerException
+ void testRoundRobinThreeFA() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
construct(Race.class, "Bear");
@@ -289,10 +266,11 @@ public void testRoundRobinThreeFA() throws PersistenceLayerException
runRoundRobin("Familiar|Bear|FOLLOWERADJUSTMENT:-6",
"Familiar|Lion|FOLLOWERADJUSTMENT:-4",
"Familiar|Tiger|FOLLOWERADJUSTMENT:-5");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinTwoType() throws PersistenceLayerException
+ void testRoundRobinTwoType() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
construct(CompanionList.class, "Companion");
@@ -300,49 +278,54 @@ public void testRoundRobinTwoType() throws PersistenceLayerException
construct(Race.class, "Tiger");
runRoundRobin("Companion|Lion|FOLLOWERADJUSTMENT:-5",
"Familiar|Tiger|FOLLOWERADJUSTMENT:-5");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinComplex() throws PersistenceLayerException
+ void testRoundRobinComplex() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
construct(Race.class, "Lion");
construct(Race.class, "Tiger");
runRoundRobin("Familiar|Lion,Tiger|FOLLOWERADJUSTMENT:-3|!PRECLASS:1,Cleric=1|PRERACE:1,Human");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinTwoPRE() throws PersistenceLayerException
+ void testRoundRobinTwoPRE() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
construct(Race.class, "Lion");
construct(Race.class, "Tiger");
runRoundRobin("Familiar|Lion|FOLLOWERADJUSTMENT:-5",
"Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRERACE:1,Human");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinDupePre() throws PersistenceLayerException
+ void testRoundRobinDupePre() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
construct(Race.class, "Tiger");
runRoundRobin(
"Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRECLASS:1,Cleric=1",
"Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRERACE:1,Human");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinDupePreDiffFA() throws PersistenceLayerException
+ void testRoundRobinDupePreDiffFA() throws PersistenceLayerException
{
construct(CompanionList.class, "Familiar");
construct(Race.class, "Tiger");
runRoundRobin(
"Familiar|Tiger|FOLLOWERADJUSTMENT:-3|PRECLASS:1,Cleric=1",
"Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRERACE:1,Human");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Test
- public void testRoundRobinReal() throws PersistenceLayerException
+ void testRoundRobinReal() throws PersistenceLayerException
{
construct(CompanionList.class, "Psicrystal");
construct(Race.class, "Psicrystal (Single Minded)");
@@ -365,6 +348,7 @@ public void testRoundRobinReal() throws PersistenceLayerException
+ "Psicrystal (Hero),Psicrystal (Liar),Psicrystal (Meticulous),Psicrystal (Nimble),Psicrystal (Observant),"
+ "Psicrystal (Poised),Psicrystal (Resolved),Psicrystal (Sage),Psicrystal (Single Minded),"
+ "Psicrystal (Sneaky),Psicrystal (Sympathetic)");
+ assertNotNull(getWriteToken().unparse(primaryContext, primaryProf));
}
@Override
@@ -384,17 +368,4 @@ protected ConsolidationRule getConsolidationRule()
{
return ConsolidationRule.SEPARATE;
}
-
-// private void buildCompanionMod(String type)
-// {
-// String mod = "isAMod";
-// ReferenceContext ref1 = primaryContext.ref;
-// ReferenceContext ref2 = secondaryContext.ref;
-// CompanionList cl1 = ref1.silentlyGetConstructedCDOMObject(CompanionList.class, type);
-// CompanionList cl2 = ref2.silentlyGetConstructedCDOMObject(CompanionList.class, type);
-// CompanionMod cm1 = ref1.constructCDOMObject(CompanionMod.class, mod);
-// CompanionMod cm2 = ref2.constructCDOMObject(CompanionMod.class, mod);
-// ref1.reassociateCategory(cl1, cm1);
-// ref1.reassociateCategory(cl2, cm2);
-// }
}