* A list of names seperated by ".", such as "variableOrFact.fact.entity.variable",
diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/variable/ShadowVariable.java b/core/src/main/java/ai/timefold/solver/core/api/domain/variable/ShadowVariable.java
index a2fa0e0b09f..4ac06c9b5a9 100644
--- a/core/src/main/java/ai/timefold/solver/core/api/domain/variable/ShadowVariable.java
+++ b/core/src/main/java/ai/timefold/solver/core/api/domain/variable/ShadowVariable.java
@@ -11,8 +11,8 @@
/**
* Specifies that a bean property (or a field) is a custom shadow variable of 1 or more source variables.
- * The source variable may be a genuine {@link PlanningVariable}, {@link PlanningListVariable},
- * or another shadow variable.
+ * The source variable may be a genuine {@link PlanningVariable} or another shadow variable.
+ * A genuine {@link PlanningListVariable} cannot be a source.
*
* It is specified on a getter of a java bean property (or a field) of a {@link PlanningEntity} class.
*/
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java
index de226d1cdb7..9eaa2477f41 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSource.java
@@ -10,6 +10,7 @@
import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable;
import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable;
+import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable;
import ai.timefold.solver.core.api.domain.variable.ShadowSources;
@@ -126,6 +127,16 @@ public static RootVariableSource from(
}
var isVariable = isVariable(solutionMetaModel, memberAccessor.getDeclaringClass(), pathPart.name());
+ if (isVariable
+ && getAnnotation(memberAccessor.getDeclaringClass(), pathPart.name(),
+ PlanningListVariable.class) != null) {
+ throw new IllegalArgumentException(
+ """
+ The source path (%s) starting from root class (%s) accesses a planning list variable (%s), which is not allowed.
+ Maybe remove the source path (%s) from the @%s?"""
+ .formatted(variablePath, rootEntityClass.getSimpleName(), pathPart.name(),
+ variablePath, ShadowSources.class.getSimpleName()));
+ }
chainToVariable.add(memberAccessor);
for (var chain : chainStartingFromSourceVariableList) {
chain.add(memberAccessor);
diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSourceTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSourceTest.java
index 0c64370ecc9..5379ad53114 100644
--- a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSourceTest.java
+++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/declarative/RootVariableSourceTest.java
@@ -548,6 +548,23 @@ void invalidPathUsingGroupAfterGroup() {
" after another collection (group), which is not allowed.");
}
+ @Test
+ void invalidPathUsingBareListVariable() {
+ assertThatCode(() -> RootVariableSource.from(
+ planningSolutionMetaModel,
+ TestdataInvalidDeclarativeEntity.class,
+ "shadow",
+ "values",
+ DEFAULT_MEMBER_ACCESSOR_FACTORY,
+ DEFAULT_DESCRIPTOR_POLICY))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContainingAll(
+ """
+ The source path (values) starting from root class (TestdataInvalidDeclarativeEntity) \
+ accesses a planning list variable (values), which is not allowed.""",
+ "Maybe remove the source path (values) from the @ShadowSources?");
+ }
+
@Test
void invalidPathUsingGroupAfterVariable() {
assertThatCode(() -> RootVariableSource.from(
diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/invalid/TestdataInvalidDeclarativeEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/invalid/TestdataInvalidDeclarativeEntity.java
index 647abb9152a..d8f36caae75 100644
--- a/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/invalid/TestdataInvalidDeclarativeEntity.java
+++ b/core/src/test/java/ai/timefold/solver/core/testdomain/shadow/invalid/TestdataInvalidDeclarativeEntity.java
@@ -10,6 +10,8 @@
@PlanningEntity
public class TestdataInvalidDeclarativeEntity extends TestdataObject {
+ TestdataInvalidDeclarativeEntity fact;
+
@PlanningListVariable
List values;
@@ -23,9 +25,17 @@ public TestdataInvalidDeclarativeEntity(String code) {
super(code);
}
- @ShadowSources("values")
+ @ShadowSources("fact.shadow")
public Integer shadowSupplier() {
- return values.size();
+ return fact == null ? 0 : fact.getShadow();
+ }
+
+ public TestdataInvalidDeclarativeEntity getFact() {
+ return fact;
+ }
+
+ public void setFact(TestdataInvalidDeclarativeEntity fact) {
+ this.fact = fact;
}
public List getValues() {
diff --git a/docs/src/modules/ROOT/pages/domain-modeling/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/domain-modeling/modeling-planning-problems.adoc
index d79b4689052..adc7ff8c69b 100644
--- a/docs/src/modules/ROOT/pages/domain-modeling/modeling-planning-problems.adoc
+++ b/docs/src/modules/ROOT/pages/domain-modeling/modeling-planning-problems.adoc
@@ -1800,7 +1800,7 @@ These paths must follow 1 of 3 syntactic forms:
|Simple Variable Name
|"variableName"
-|Refers to a variable (genuine or shadow) on the same planning entity.
+|Refers to a variable (genuine or shadow) on the same planning entity. A planning list variable cannot be used as a source.
|Chained Property Path
|"a.b.c"