diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java index 15fdf450..91624cd8 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java +++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java @@ -8,6 +8,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.util.EContentsEList; import org.eclipse.xtext.util.IResourceScopeCache; import org.eclipse.xtext.util.Tuples; import tools.refinery.language.model.problem.Problem; @@ -51,8 +52,14 @@ public static Map computeReferenceCounts(EObject root) { } private static void countCrossReferences(EObject eObject, Map map) { - for (var referencedObject : eObject.eCrossReferences()) { - map.compute(referencedObject, (key, currentValue) -> currentValue == null ? 1 : currentValue + 1); + var featureIterator = (EContentsEList.FeatureIterator) eObject.eCrossReferences().iterator(); + while (featureIterator.hasNext()) { + var referencedObject = featureIterator.next(); + // Avoid double-counting the derived reference {@code variableOrNode} of {@code VariableOrNodeExpression}, + // as the original reference is just {@code element}. + if (!featureIterator.feature().isDerived()) { + map.compute(referencedObject, (key, currentValue) -> currentValue == null ? 1 : currentValue + 1); + } } } } diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/VariableValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/VariableValidationTest.java new file mode 100644 index 00000000..ae5f9e4f --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/VariableValidationTest.java @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.tests.validation; + +import com.google.inject.Inject; +import org.eclipse.emf.common.util.Diagnostic; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import tools.refinery.language.tests.InjectWithRefinery; +import tools.refinery.language.tests.utils.ProblemParseHelper; +import tools.refinery.language.validation.ProblemValidator; + +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@InjectWithRefinery +class VariableValidationTest { + @Inject + private ProblemParseHelper parseHelper; + + @Test + void shouldBeSingletonVariableTest() { + var problem = parseHelper.parse(""" + pred foo(a, b). + + pred bar(a) <-> foo(a, shouldBeSingleton). + """); + var issues = problem.validate(); + assertThat(issues, hasItem(allOf( + hasProperty("severity", is(Diagnostic.WARNING)), + hasProperty("issueCode", is(ProblemValidator.SINGLETON_VARIABLE_ISSUE)), + hasProperty("message", containsString("shouldBeSingleton")) + ))); + } + + @ParameterizedTest + @ValueSource(strings = {"_shouldBeSingleton", "_"}) + void singletonVariableTest(String variableName) { + var problem = parseHelper.parse(""" + pred foo(a, b). + + pred bar(a) <-> foo(a, %s). + """.formatted(variableName)); + var issues = problem.validate(); + assertThat(issues, not(hasItem( + hasProperty("issueCode", is(ProblemValidator.SINGLETON_VARIABLE_ISSUE))))); + } + + @ParameterizedTest + @MethodSource + void shouldBeAtomNodeTest(String declaration) { + var problem = parseHelper.parse(""" + pred foo(a, b). + + pred bar(a) <-> foo(a, shouldBeAtom). + + %s + """.formatted(declaration)); + var issues = problem.validate(); + assertThat(issues, hasItem(allOf( + hasProperty("severity", is(Diagnostic.ERROR)), + hasProperty("issueCode", is(ProblemValidator.NODE_CONSTANT_ISSUE)), + hasProperty("message", containsString("shouldBeAtom")) + ))); + } + + public static Stream shouldBeAtomNodeTest() { + return Stream.of( + Arguments.of("node(shouldBeAtom)."), + Arguments.of("declare shouldBeAtom."), + Arguments.of("multi shouldBeAtom.") + ); + } + + @ParameterizedTest + @MethodSource("shouldBeAtomNodeTest") + void shouldBeAtomNodeRuleTest(String declaration) { + var problem = parseHelper.parse(""" + pred foo(a, b). + + rule bar(a) ==> foo(a, shouldBeAtom). + + %s + """.formatted(declaration)); + var issues = problem.validate(); + assertThat(issues, hasItem(allOf( + hasProperty("severity", is(Diagnostic.ERROR)), + hasProperty("issueCode", is(ProblemValidator.NODE_CONSTANT_ISSUE)), + hasProperty("message", containsString("shouldBeAtom")) + ))); + } +}