From 753cbd554139695d89665ab1baeb03abe09e7b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Sun, 19 Jul 2026 13:34:39 +0200 Subject: [PATCH] fix(format): guard against null members in FormatJvmModelInferrer.inferConstants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inferConstants added createConstant(format, c) to the JVM model unconditionally. createConstant returns null for a value-less constant (its switch fall-through), so a null JvmMember reached it.getMembers().add(...) — which the EMF EList rejects with IllegalArgumentException: The 'no null' constraint is violated (or leaks a null member downstream). Regression class: the original Xtend `members += allConstants.map[createConstant]` compiled to JvmTypesBuilder.operator_add, which silently skips nulls; the Xtend-to-Java migration translated it to a bare loop-add that does not. Guard the add (equivalent to operator_add's null-skip). Add FormatJvmModelInferrerTest pinning the behaviour: a value-less constant must not produce a null member or throw. A/B verified — the test fails on the pre-fix code (no null constraint violated) and passes with the guard. Found by an archaeology audit of the merged Xtend→Java migrations; the skill rule that prevents this class is added separately (rules/10 §10.5). Co-Authored-By: Claude Fable 5 --- .../jvmmodel/FormatJvmModelInferrerTest.java | 74 +++++++++++++++++++ .../xtext/test/format/FormatTestSuite.java | 3 +- .../jvmmodel/FormatJvmModelInferrer.java | 5 +- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrerTest.java diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrerTest.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrerTest.java new file mode 100644 index 0000000000..f5990ea105 --- /dev/null +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrerTest.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.xtext.format.jvmmodel; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.impl.ResourceImpl; +import org.eclipse.xtext.common.types.JvmGenericType; +import org.eclipse.xtext.common.types.TypesFactory; +import org.junit.jupiter.api.Test; + +import com.avaloq.tools.ddk.xtext.format.format.Constant; +import com.avaloq.tools.ddk.xtext.format.format.FormatConfiguration; +import com.avaloq.tools.ddk.xtext.format.format.FormatFactory; +import com.avaloq.tools.ddk.xtext.test.format.util.FormatTestUtil; +import com.avaloq.tools.ddk.xtext.test.jupiter.AbstractXtextTest; + + +/** + * Regression test for {@link FormatJvmModelInferrer#inferConstants(FormatConfiguration, JvmGenericType)}. + * + *

A {@link Constant} without an {@code intValue} or a {@code stringValue} makes + * {@link FormatJvmModelInferrer#createConstant(FormatConfiguration, Constant)} return {@code null}. The inferrer must + * skip such {@code null} members instead of leaking them into the JVM model (mirroring the original Xtend {@code +=} + * null-skip), otherwise an {@code IllegalArgumentException: element: null} surfaces downstream.

+ */ +@SuppressWarnings("nls") +public class FormatJvmModelInferrerTest extends AbstractXtextTest { + + private final FormatJvmModelInferrer inferrer = getXtextTestUtil().get(FormatJvmModelInferrer.class); + + @Override + protected FormatTestUtil getXtextTestUtil() { + return FormatTestUtil.getInstance(); + } + + @Override + protected String getTestSourceFileName() { + return null; // the model is fabricated in-memory, no source file is loaded + } + + /** + * A value-less constant must not leak a {@code null} member into the inferred type. + */ + @Test + public void testInferConstantsSkipsValuelessConstant() { + final FormatConfiguration format = FormatFactory.eINSTANCE.createFormatConfiguration(); + final Constant valuelessConstant = FormatFactory.eINSTANCE.createConstant(); + valuelessConstant.setName("VALUELESS"); // name only, both intValue and stringValue stay null + format.getConstants().add(valuelessConstant); + + final Resource resource = new ResourceImpl(URI.createURI("synthetic:/format/valueless.format")); + resource.getContents().add(format); + + // createConstant returns null for a value-less constant. + assertNull(inferrer.createConstant(format, valuelessConstant), "value-less constant should yield no JVM member"); + + final JvmGenericType type = TypesFactory.eINSTANCE.createJvmGenericType(); + assertDoesNotThrow(() -> inferrer.inferConstants(format, type), "inferConstants must not fail on a value-less constant"); + assertFalse(type.getMembers().contains(null), "the inferred type must not contain a null member"); + } +} diff --git a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java index 8b12ea9435..4a4ef63c53 100644 --- a/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java +++ b/com.avaloq.tools.ddk.xtext.format.test/src/com/avaloq/tools/ddk/xtext/test/format/FormatTestSuite.java @@ -16,6 +16,7 @@ import com.avaloq.tools.ddk.xtext.format.FormatParsingTest; import com.avaloq.tools.ddk.xtext.format.builder.FormatBuilderParticipantTest; import com.avaloq.tools.ddk.xtext.format.formatting.FormatFormattingTest; +import com.avaloq.tools.ddk.xtext.format.jvmmodel.FormatJvmModelInferrerTest; import com.avaloq.tools.ddk.xtext.format.scoping.FormatScopingTest; import com.avaloq.tools.ddk.xtext.format.validation.FormatValidationTest; @@ -24,7 +25,7 @@ * Empty class serving only as holder for JUnit5 annotations. */ @Suite -@SelectClasses({FormatParsingTest.class, FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class}) +@SelectClasses({FormatParsingTest.class, FormatFormattingTest.class, FormatValidationTest.class, FormatScopingTest.class, FormatBuilderParticipantTest.class, FormatJvmModelInferrerTest.class}) public class FormatTestSuite { } diff --git a/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrer.java b/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrer.java index 3abd284dce..e42896966c 100644 --- a/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrer.java +++ b/com.avaloq.tools.ddk.xtext.format/src/com/avaloq/tools/ddk/xtext/format/jvmmodel/FormatJvmModelInferrer.java @@ -211,7 +211,10 @@ public void inferClass(final FormatConfiguration format, final JvmGenericType it public void inferConstants(final FormatConfiguration format, final JvmGenericType it) { if (!FormatGeneratorUtil.getAllConstants(format).isEmpty()) { for (final Constant c : FormatGeneratorUtil.getAllConstants(format)) { - it.getMembers().add(createConstant(format, c)); + final JvmMember member = createConstant(format, c); + if (member != null) { // createConstant returns null for a value-less constant; the original Xtend += (operator_add) skipped nulls + it.getMembers().add(member); + } } } }