From 273231e29fc9be942bf448b740958483016978e4 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Mon, 6 Jul 2026 20:32:37 +0200 Subject: [PATCH 01/14] Fix redundant CD2Pojo association check --- .../cdgen/cocos/CDAssociationUnique.java | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java index f4144774b..9d218dd86 100644 --- a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java +++ b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java @@ -26,45 +26,19 @@ public void check(ASTCDDefinition node) { List alreadyChecked = new ArrayList<>(); // we check for each pair of associations - for (ASTCDAssociation assoc1 : node.getCDAssociationsList()) { + for (ASTCDAssociation assoc2 : node.getCDAssociationsList()) { - alreadyChecked.add(assoc1); - - for (ASTCDAssociation assoc2 : node.getCDAssociationsList()) { - - // only check each pair once - if (assoc2 != assoc1 && !alreadyChecked.contains(assoc2)) { - - // if they share a left role-name, the referenced types on the right should not be the - // same - if (deriveRoleName(assoc1, AssocSide.LEFT).equals(deriveRoleName(assoc2, - AssocSide.LEFT))) { - checkRef(node, findTypeByFullName(assoc1, assoc1.getRightQualifiedName().getQName()), - findTypeByFullName(assoc2, assoc2.getRightQualifiedName().getQName()), assoc1); - } - - // if they share a right role-name, the referenced types on the left should not be the - // same - if (deriveRoleName(assoc1, AssocSide.RIGHT).equals(deriveRoleName(assoc2, - AssocSide.RIGHT))) { - checkRef(node, findTypeByFullName(assoc1, assoc1.getLeftQualifiedName().getQName()), - findTypeByFullName(assoc2, assoc2.getLeftQualifiedName().getQName()), assoc1); - } - - // We also consider a left-to-right role name match ... - if (deriveRoleName(assoc1, AssocSide.LEFT).equals(deriveRoleName(assoc2, - AssocSide.RIGHT))) { - checkRef(node, findTypeByFullName(assoc1, assoc1.getRightQualifiedName().getQName()), - findTypeByFullName(assoc2, assoc2.getLeftQualifiedName().getQName()), assoc1); - } - // ... as well as a right-to-left match - if (deriveRoleName(assoc1, AssocSide.RIGHT).equals(deriveRoleName(assoc2, - AssocSide.LEFT))) { - checkRef(node, findTypeByFullName(assoc1, assoc1.getLeftQualifiedName().getQName()), - findTypeByFullName(assoc2, assoc2.getRightQualifiedName().getQName()), assoc1); + for (ASTCDAssociation assoc1 : alreadyChecked) { + for (AssociationReference ref1 : getAssociationReferences(assoc1)) { + for (AssociationReference ref2 : getAssociationReferences(assoc2)) { + if (ref1.name.equals(ref2.name)) { + checkRef(node, ref1.sourceType, ref2.sourceType, assoc2); + } } } } + + alreadyChecked.add(assoc2); } } @@ -85,6 +59,9 @@ protected ASTCDType findTypeByFullName(ASTCDAssociation node, String fullName) { /** Check if type2 is the same as type1. */ protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, ASTCDAssociation assoc1) { + if (type1 == null || type2 == null) { + return; + } if (type1.equals(type2)) { Log.error(String.format("0xCDCE1: %s has a duplicate association to %s", type1.getName(), type2.getName()), assoc1.isPresent_SourcePositionStart() ? assoc1 @@ -102,20 +79,52 @@ protected String deriveRoleName(ASTCDAssociation assoc, AssocSide side) { else { assocSide = assoc.getRight(); } - if (assocSide.isPresentCDRole()) { - return assocSide.getCDRole().getName(); - } - else if (assoc.isPresentName()) { + if (assoc.isPresentName()) { return StringUtils.uncapitalize(assoc.getName()); } + else if (assocSide.isPresentCDRole()) { + return assocSide.getCDRole().getName(); + } else { return StringUtils.uncapitalize(assocSide.getMCQualifiedType().getMCQualifiedName() .getBaseName()); } } + protected List getAssociationReferences(ASTCDAssociation assoc) { + List references = new ArrayList<>(); + + boolean navigableLeft = assoc.getCDAssocDir().isDefinitiveNavigableLeft(); + boolean navigableRight = assoc.getCDAssocDir().isDefinitiveNavigableRight(); + boolean undirected = !navigableLeft && !navigableRight; + + if (navigableRight || undirected) { + references.add(new AssociationReference(findTypeByFullName(assoc, assoc + .getLeftQualifiedName().getQName()), deriveRoleName(assoc, AssocSide.RIGHT))); + } + if (navigableLeft || undirected) { + references.add(new AssociationReference(findTypeByFullName(assoc, assoc + .getRightQualifiedName().getQName()), deriveRoleName(assoc, AssocSide.LEFT))); + } + + return references; + } + private enum AssocSide { LEFT, RIGHT; } + protected static class AssociationReference { + + protected final ASTCDType sourceType; + + protected final String name; + + protected AssociationReference(ASTCDType sourceType, String name) { + this.sourceType = sourceType; + this.name = name; + } + + } + } From 9241ed6ed490b1e66d539e73bbbb6d74bb048e4c Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Mon, 6 Jul 2026 20:33:25 +0200 Subject: [PATCH 02/14] add tests --- .../cd/cdgen/BuilderDecoratorTest.java | 26 +++++++++++-------- .../cdgen/cocos/CDAssociationUniqueTest.java | 14 ++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java index 719647678..e3c31193d 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java @@ -9,6 +9,7 @@ import de.monticore.generating.templateengine.GlobalExtensionManagement; import de.monticore.io.paths.MCPath; import de.monticore.runtime.junit.MCAssertions; +add import de.se_rwth.commons.logging.Log; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; import java.nio.file.Paths; @@ -19,7 +20,7 @@ import java.util.Optional; class BuilderDecoratorTest extends AbstractDecoratorTest { - + @Test public void testBuilder() throws Exception { var opt = CD4CodeMill.parser() @@ -61,17 +62,20 @@ public void testBuilder() throws Exception { + " }\n" + "}"); // @formatter:on - + Assertions.assertTrue(opt.isPresent()); - + super.doTest(opt.get()); - - for (int i = 0; i < 7; i++) // Test, that the warning about missing setters is present - MCAssertions.assertHasFinding(f -> f.getMsg().startsWith("Requested setter of TestBuilder") - && f.isWarning()); + + // Test that the warnings about missing setters are present. + Assertions.assertEquals(7, Log.getFindings().stream().filter(f -> f.getMsg().startsWith( + "Requested setter of TestBuilder") && f.isWarning()).count()); + Assertions.assertTrue(Log.getFindings().stream().allMatch(f -> f.getMsg().startsWith( + "Requested setter of TestBuilder") && f.isWarning())); + Log.clearFindings(); MCAssertions.assertNoFindings(); } - + @Test public void testTemplateExistence() { //test existence of the templates @@ -85,12 +89,12 @@ public void testTemplateExistence() { Assertions.assertTrue(Files.exists(temPath)); } } - + @Override protected Optional getHandWrittenPath() { return Optional.of(new MCPath("src/cdGenIntTestHwc/java")); } - + @Override public void initializeDecConf(GlobalExtensionManagement glex, DecoratorConfig config, GeneratorSetup setup) { @@ -107,5 +111,5 @@ public void initializeDecConf(GlobalExtensionManagement glex, DecoratorConfig co config.withDecorator(new CardinalityDefaultDecorator()); config.configDefault(CardinalityDefaultDecorator.class, MatchResult.APPLY); } - + } diff --git a/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java index bf2ce95dd..521ddcc60 100644 --- a/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java +++ b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java @@ -42,6 +42,13 @@ public void testDuplicatesWithImplicitRoles() throws IOException { runTestForErrorCode(model, ERROR_CODE); } + @Test + public void testUniqueExplicitRole() throws IOException { + String model = "classdiagram UniqueAssocs {" + " class A; class B;" + + " association A -> B;" + " association A -> (other) B;" + "}"; + runTest(model, false); + } + @Test public void testDuplicatesInReverse() throws IOException { String model = "classdiagram DuplicateAssocs {" + " class A; class B;" @@ -56,4 +63,11 @@ public void testUniqueAssocName() throws IOException { runTest(model, false); } + @Test + public void testUniqueAssocNameWithSameRole() throws IOException { + String model = "classdiagram UniqueAssocs {" + " class A; class B;" + + " association assoc1 A -> (b) B;" + " association assoc2 A -> (b) B;" + "}"; + runTest(model, false); + } + } From 22aec90c8c540da2a47fd40e73dda5d9b9671cd4 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Tue, 7 Jul 2026 00:51:49 +0200 Subject: [PATCH 03/14] resolve conflicts --- .../de/monticore/cd/cdgen/BuilderDecoratorTest.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java index 9691a9c54..25cf24bf2 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java @@ -9,6 +9,7 @@ import de.monticore.generating.templateengine.GlobalExtensionManagement; import de.monticore.io.paths.MCPath; import de.monticore.runtime.junit.MCAssertions; +import de.se_rwth.commons.logging.Log; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; import java.nio.file.Paths; @@ -71,9 +72,12 @@ class Level2class implements Level1Interface{ super.doTest(opt.get()); - for (int i = 0; i < 7; i++) // Test, that the warning about missing setters is present - MCAssertions.assertHasFinding(f -> f.getMsg().startsWith("Requested setter of TestBuilder") - && f.isWarning()); + // Test that the warnings about missing setters are present. + Assertions.assertEquals(7, Log.getFindings().stream().filter(f -> f.getMsg().startsWith( + "Requested setter of TestBuilder") && f.isWarning()).count()); + Assertions.assertTrue(Log.getFindings().stream().allMatch(f -> f.getMsg().startsWith( + "Requested setter of TestBuilder") && f.isWarning())); + Log.clearFindings(); } @Test From 2d447825db1dfd22bb5b89ba7fb3cc3f18841946 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Tue, 7 Jul 2026 01:31:06 +0200 Subject: [PATCH 04/14] fix formating --- .../java/de/monticore/cdgen/cocos/CDAssociationUnique.java | 4 ++-- .../test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java | 1 - .../de/monticore/cdgen/cocos/CDAssociationUniqueTest.java | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java index 9d218dd86..bc753c968 100644 --- a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java +++ b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java @@ -99,8 +99,8 @@ protected List getAssociationReferences(ASTCDAssociation a boolean undirected = !navigableLeft && !navigableRight; if (navigableRight || undirected) { - references.add(new AssociationReference(findTypeByFullName(assoc, assoc - .getLeftQualifiedName().getQName()), deriveRoleName(assoc, AssocSide.RIGHT))); + references.add(new AssociationReference(findTypeByFullName(assoc, assoc.getLeftQualifiedName() + .getQName()), deriveRoleName(assoc, AssocSide.RIGHT))); } if (navigableLeft || undirected) { references.add(new AssociationReference(findTypeByFullName(assoc, assoc diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java index 25cf24bf2..a110db7ba 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java @@ -8,7 +8,6 @@ import de.monticore.generating.GeneratorSetup; import de.monticore.generating.templateengine.GlobalExtensionManagement; import de.monticore.io.paths.MCPath; -import de.monticore.runtime.junit.MCAssertions; import de.se_rwth.commons.logging.Log; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; diff --git a/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java index 521ddcc60..e868dd47d 100644 --- a/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java +++ b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java @@ -44,8 +44,8 @@ public void testDuplicatesWithImplicitRoles() throws IOException { @Test public void testUniqueExplicitRole() throws IOException { - String model = "classdiagram UniqueAssocs {" + " class A; class B;" - + " association A -> B;" + " association A -> (other) B;" + "}"; + String model = "classdiagram UniqueAssocs {" + " class A; class B;" + " association A -> B;" + + " association A -> (other) B;" + "}"; runTest(model, false); } From 6e1e608726caab83a4c0b6eca0d0caf50a930ace Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Tue, 7 Jul 2026 13:38:49 +0200 Subject: [PATCH 05/14] move changes to a new coco --- .../cocos/CDAssociationReferenceUnique.java | 102 ++++++++++++++++++ .../cdgen/cocos/CDAssociationUnique.java | 87 +++++++-------- .../CDAssociationReferenceUniqueTest.java | 69 ++++++++++++ .../cdgen/cocos/CDAssociationUniqueTest.java | 14 --- 4 files changed, 210 insertions(+), 62 deletions(-) create mode 100644 cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java create mode 100644 cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationReferenceUniqueTest.java diff --git a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java new file mode 100644 index 000000000..05efbc3df --- /dev/null +++ b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java @@ -0,0 +1,102 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.cdgen.cocos; + +import de.monticore.cdassociation._ast.ASTCDAssocSide; +import de.monticore.cdassociation._ast.ASTCDAssociation; +import de.monticore.cdbasis._ast.ASTCDDefinition; +import de.monticore.cdbasis._ast.ASTCDType; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; + +/** + * Checks that generated association references are unique for CD2Java/CD2Pojo. + */ +public class CDAssociationReferenceUnique extends CDAssociationUniqueInHierarchy { + + @Override + public void check(ASTCDDefinition node) { + List alreadyChecked = new ArrayList<>(); + + // we check for each pair of associations + for (ASTCDAssociation assoc2 : node.getCDAssociationsList()) { + for (ASTCDAssociation assoc1 : alreadyChecked) { + for (AssociationReference ref1 : getAssociationReferences(assoc1)) { + for (AssociationReference ref2 : getAssociationReferences(assoc2)) { + if (ref1.name.equals(ref2.name)) { + checkRef(node, ref1.sourceType, ref2.sourceType, assoc2); + } + } + } + } + + alreadyChecked.add(assoc2); + } + } + + @Override + protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, + ASTCDAssociation assoc1) { + if (type1 == null || type2 == null) { + return; + } + super.checkRef(node, type1, type2, assoc1); + } + + protected List getAssociationReferences(ASTCDAssociation assoc) { + List references = new ArrayList<>(); + + boolean navigableLeft = assoc.getCDAssocDir().isDefinitiveNavigableLeft(); + boolean navigableRight = assoc.getCDAssocDir().isDefinitiveNavigableRight(); + boolean undirected = !navigableLeft && !navigableRight; + + if (navigableRight || undirected) { + references.add(new AssociationReference(findTypeByFullName(assoc, assoc.getLeftQualifiedName() + .getQName()), deriveReferenceName(assoc, AssocSide.RIGHT))); + } + if (navigableLeft || undirected) { + references.add(new AssociationReference(findTypeByFullName(assoc, assoc + .getRightQualifiedName().getQName()), deriveReferenceName(assoc, AssocSide.LEFT))); + } + + return references; + } + + protected String deriveReferenceName(ASTCDAssociation assoc, AssocSide side) { + ASTCDAssocSide assocSide; + if (side.equals(AssocSide.LEFT)) { + assocSide = assoc.getLeft(); + } + else { + assocSide = assoc.getRight(); + } + if (assoc.isPresentName()) { + return StringUtils.uncapitalize(assoc.getName()); + } + else if (assocSide.isPresentCDRole()) { + return assocSide.getCDRole().getName(); + } + else { + return StringUtils.uncapitalize(assocSide.getMCQualifiedType().getMCQualifiedName() + .getBaseName()); + } + } + + protected enum AssocSide { + LEFT, RIGHT; + } + + protected static class AssociationReference { + + protected final ASTCDType sourceType; + + protected final String name; + + protected AssociationReference(ASTCDType sourceType, String name) { + this.sourceType = sourceType; + this.name = name; + } + + } + +} diff --git a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java index bc753c968..f4144774b 100644 --- a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java +++ b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationUnique.java @@ -26,19 +26,45 @@ public void check(ASTCDDefinition node) { List alreadyChecked = new ArrayList<>(); // we check for each pair of associations - for (ASTCDAssociation assoc2 : node.getCDAssociationsList()) { + for (ASTCDAssociation assoc1 : node.getCDAssociationsList()) { - for (ASTCDAssociation assoc1 : alreadyChecked) { - for (AssociationReference ref1 : getAssociationReferences(assoc1)) { - for (AssociationReference ref2 : getAssociationReferences(assoc2)) { - if (ref1.name.equals(ref2.name)) { - checkRef(node, ref1.sourceType, ref2.sourceType, assoc2); - } + alreadyChecked.add(assoc1); + + for (ASTCDAssociation assoc2 : node.getCDAssociationsList()) { + + // only check each pair once + if (assoc2 != assoc1 && !alreadyChecked.contains(assoc2)) { + + // if they share a left role-name, the referenced types on the right should not be the + // same + if (deriveRoleName(assoc1, AssocSide.LEFT).equals(deriveRoleName(assoc2, + AssocSide.LEFT))) { + checkRef(node, findTypeByFullName(assoc1, assoc1.getRightQualifiedName().getQName()), + findTypeByFullName(assoc2, assoc2.getRightQualifiedName().getQName()), assoc1); + } + + // if they share a right role-name, the referenced types on the left should not be the + // same + if (deriveRoleName(assoc1, AssocSide.RIGHT).equals(deriveRoleName(assoc2, + AssocSide.RIGHT))) { + checkRef(node, findTypeByFullName(assoc1, assoc1.getLeftQualifiedName().getQName()), + findTypeByFullName(assoc2, assoc2.getLeftQualifiedName().getQName()), assoc1); + } + + // We also consider a left-to-right role name match ... + if (deriveRoleName(assoc1, AssocSide.LEFT).equals(deriveRoleName(assoc2, + AssocSide.RIGHT))) { + checkRef(node, findTypeByFullName(assoc1, assoc1.getRightQualifiedName().getQName()), + findTypeByFullName(assoc2, assoc2.getLeftQualifiedName().getQName()), assoc1); + } + // ... as well as a right-to-left match + if (deriveRoleName(assoc1, AssocSide.RIGHT).equals(deriveRoleName(assoc2, + AssocSide.LEFT))) { + checkRef(node, findTypeByFullName(assoc1, assoc1.getLeftQualifiedName().getQName()), + findTypeByFullName(assoc2, assoc2.getRightQualifiedName().getQName()), assoc1); } } } - - alreadyChecked.add(assoc2); } } @@ -59,9 +85,6 @@ protected ASTCDType findTypeByFullName(ASTCDAssociation node, String fullName) { /** Check if type2 is the same as type1. */ protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, ASTCDAssociation assoc1) { - if (type1 == null || type2 == null) { - return; - } if (type1.equals(type2)) { Log.error(String.format("0xCDCE1: %s has a duplicate association to %s", type1.getName(), type2.getName()), assoc1.isPresent_SourcePositionStart() ? assoc1 @@ -79,52 +102,20 @@ protected String deriveRoleName(ASTCDAssociation assoc, AssocSide side) { else { assocSide = assoc.getRight(); } - if (assoc.isPresentName()) { - return StringUtils.uncapitalize(assoc.getName()); - } - else if (assocSide.isPresentCDRole()) { + if (assocSide.isPresentCDRole()) { return assocSide.getCDRole().getName(); } + else if (assoc.isPresentName()) { + return StringUtils.uncapitalize(assoc.getName()); + } else { return StringUtils.uncapitalize(assocSide.getMCQualifiedType().getMCQualifiedName() .getBaseName()); } } - protected List getAssociationReferences(ASTCDAssociation assoc) { - List references = new ArrayList<>(); - - boolean navigableLeft = assoc.getCDAssocDir().isDefinitiveNavigableLeft(); - boolean navigableRight = assoc.getCDAssocDir().isDefinitiveNavigableRight(); - boolean undirected = !navigableLeft && !navigableRight; - - if (navigableRight || undirected) { - references.add(new AssociationReference(findTypeByFullName(assoc, assoc.getLeftQualifiedName() - .getQName()), deriveRoleName(assoc, AssocSide.RIGHT))); - } - if (navigableLeft || undirected) { - references.add(new AssociationReference(findTypeByFullName(assoc, assoc - .getRightQualifiedName().getQName()), deriveRoleName(assoc, AssocSide.LEFT))); - } - - return references; - } - private enum AssocSide { LEFT, RIGHT; } - protected static class AssociationReference { - - protected final ASTCDType sourceType; - - protected final String name; - - protected AssociationReference(ASTCDType sourceType, String name) { - this.sourceType = sourceType; - this.name = name; - } - - } - } diff --git a/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationReferenceUniqueTest.java b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationReferenceUniqueTest.java new file mode 100644 index 000000000..06ee230d0 --- /dev/null +++ b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationReferenceUniqueTest.java @@ -0,0 +1,69 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.cdgen.cocos; + +import de.monticore.cd4code._cocos.CD4CodeCoCoChecker; +import de.monticore.cd4code.cocos.AbstractJavaGenCoCoTest; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +public class CDAssociationReferenceUniqueTest extends AbstractJavaGenCoCoTest { + + @Override + protected CD4CodeCoCoChecker createChecker() { + CD4CodeCoCoChecker checker = new CD4CodeCoCoChecker(); + checker.addCoCo(new CDAssociationReferenceUnique()); + return checker; + } + + private static final String ERROR_CODE = "0xCDCE1"; + + @Test + public void testUniqueNames() throws IOException { + String model = "classdiagram UniqueAssocs {" + " class A; class B;" + " association A -> B;" + + " association A (l) -> (r) B;" + "}"; + runTest(model, false); + } + + @Test + public void testDuplicatesWithExplicitRoles() throws IOException { + String model = "classdiagram DuplicateAssocs {" + " class A; class B;" + + " association A -> B;" + " association A -> (b) B;" + "}"; + runTestForErrorCode(model, ERROR_CODE); + } + + @Test + public void testDuplicatesWithImplicitRoles() throws IOException { + String model = "classdiagram DuplicateAssocs {" + " class A; class B;" + + " association A -> B;" + " association A -> B;" + "}"; + runTestForErrorCode(model, ERROR_CODE); + } + + @Test + public void testUniqueExplicitRole() throws IOException { + String model = "classdiagram UniqueAssocs {" + " class A; class B;" + " association A -> B;" + + " association A -> (other) B;" + "}"; + runTest(model, false); + } + + @Test + public void testDuplicatesInReverse() throws IOException { + String model = "classdiagram DuplicateAssocs {" + " class A; class B;" + + " association A -> B;" + " association B <- A;" + "}"; + runTestForErrorCode(model, ERROR_CODE); + } + + @Test + public void testUniqueAssocName() throws IOException { + String model = "classdiagram UniqueAssocs {" + " class A; class B;" + + " association assoc1 A -> B;" + " association assoc2 A -> B;" + "}"; + runTest(model, false); + } + + @Test + public void testUniqueAssocNameWithSameRole() throws IOException { + String model = "classdiagram UniqueAssocs {" + " class A; class B;" + + " association assoc1 A -> (b) B;" + " association assoc2 A -> (b) B;" + "}"; + runTest(model, false); + } + +} diff --git a/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java index e868dd47d..bf2ce95dd 100644 --- a/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java +++ b/cdlang/src/test/java/de/monticore/cdgen/cocos/CDAssociationUniqueTest.java @@ -42,13 +42,6 @@ public void testDuplicatesWithImplicitRoles() throws IOException { runTestForErrorCode(model, ERROR_CODE); } - @Test - public void testUniqueExplicitRole() throws IOException { - String model = "classdiagram UniqueAssocs {" + " class A; class B;" + " association A -> B;" - + " association A -> (other) B;" + "}"; - runTest(model, false); - } - @Test public void testDuplicatesInReverse() throws IOException { String model = "classdiagram DuplicateAssocs {" + " class A; class B;" @@ -63,11 +56,4 @@ public void testUniqueAssocName() throws IOException { runTest(model, false); } - @Test - public void testUniqueAssocNameWithSameRole() throws IOException { - String model = "classdiagram UniqueAssocs {" + " class A; class B;" - + " association assoc1 A -> (b) B;" + " association assoc2 A -> (b) B;" + "}"; - runTest(model, false); - } - } From c1013fb764a3bcd6050c5a7b2619e193085265b2 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Tue, 7 Jul 2026 13:43:32 +0200 Subject: [PATCH 06/14] remove some changes --- .../de/monticore/cd/cdgen/BuilderDecoratorTest.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java index a110db7ba..9691a9c54 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/BuilderDecoratorTest.java @@ -8,7 +8,7 @@ import de.monticore.generating.GeneratorSetup; import de.monticore.generating.templateengine.GlobalExtensionManagement; import de.monticore.io.paths.MCPath; -import de.se_rwth.commons.logging.Log; +import de.monticore.runtime.junit.MCAssertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; import java.nio.file.Paths; @@ -71,12 +71,9 @@ class Level2class implements Level1Interface{ super.doTest(opt.get()); - // Test that the warnings about missing setters are present. - Assertions.assertEquals(7, Log.getFindings().stream().filter(f -> f.getMsg().startsWith( - "Requested setter of TestBuilder") && f.isWarning()).count()); - Assertions.assertTrue(Log.getFindings().stream().allMatch(f -> f.getMsg().startsWith( - "Requested setter of TestBuilder") && f.isWarning())); - Log.clearFindings(); + for (int i = 0; i < 7; i++) // Test, that the warning about missing setters is present + MCAssertions.assertHasFinding(f -> f.getMsg().startsWith("Requested setter of TestBuilder") + && f.isWarning()); } @Test From 00eac32a5caf7fa03c91e0097a2ac9be1c7b2dd2 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Thu, 9 Jul 2026 09:50:21 +0200 Subject: [PATCH 07/14] minor --- .../cocos/CDAssociationReferenceUnique.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java index 05efbc3df..c1d4af6ec 100644 --- a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java +++ b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java @@ -10,14 +10,14 @@ import java.util.*; /** - * Checks that generated association references are unique for CD2Java/CD2Pojo. + * Checks that generated association references are unique. */ public class CDAssociationReferenceUnique extends CDAssociationUniqueInHierarchy { - + @Override public void check(ASTCDDefinition node) { List alreadyChecked = new ArrayList<>(); - + // we check for each pair of associations for (ASTCDAssociation assoc2 : node.getCDAssociationsList()) { for (ASTCDAssociation assoc1 : alreadyChecked) { @@ -29,11 +29,11 @@ public void check(ASTCDDefinition node) { } } } - + alreadyChecked.add(assoc2); } } - + @Override protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, ASTCDAssociation assoc1) { @@ -42,14 +42,14 @@ protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, } super.checkRef(node, type1, type2, assoc1); } - + protected List getAssociationReferences(ASTCDAssociation assoc) { List references = new ArrayList<>(); - + boolean navigableLeft = assoc.getCDAssocDir().isDefinitiveNavigableLeft(); boolean navigableRight = assoc.getCDAssocDir().isDefinitiveNavigableRight(); boolean undirected = !navigableLeft && !navigableRight; - + if (navigableRight || undirected) { references.add(new AssociationReference(findTypeByFullName(assoc, assoc.getLeftQualifiedName() .getQName()), deriveReferenceName(assoc, AssocSide.RIGHT))); @@ -58,10 +58,10 @@ protected List getAssociationReferences(ASTCDAssociation a references.add(new AssociationReference(findTypeByFullName(assoc, assoc .getRightQualifiedName().getQName()), deriveReferenceName(assoc, AssocSide.LEFT))); } - + return references; } - + protected String deriveReferenceName(ASTCDAssociation assoc, AssocSide side) { ASTCDAssocSide assocSide; if (side.equals(AssocSide.LEFT)) { @@ -81,22 +81,22 @@ else if (assocSide.isPresentCDRole()) { .getBaseName()); } } - + protected enum AssocSide { LEFT, RIGHT; } - + protected static class AssociationReference { - + protected final ASTCDType sourceType; - + protected final String name; - + protected AssociationReference(ASTCDType sourceType, String name) { this.sourceType = sourceType; this.name = name; } - + } - + } From 75a817a1f7d12263a217c2bee105fb3e7e363c7b Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Mon, 21 Sep 2026 10:48:42 +0200 Subject: [PATCH 08/14] Fix Java keyword association roles --- .../trafo/JavaAssociationRoleNameTrafo.java | 93 +++++++++++++++ .../java/de/monticore/cdgen/CDGenTool.java | 2 + .../cd/cdgen/DefaultCD2PojoDecoratorTest.java | 110 +++++++++++++++++- 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java diff --git a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java new file mode 100644 index 000000000..1281c8589 --- /dev/null +++ b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java @@ -0,0 +1,93 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.cd.codegen.trafo; + +import de.monticore.cd4code.CD4CodeMill; +import de.monticore.cdassociation._ast.ASTCDRole; +import de.monticore.cdassociation._symboltable.ICDAssociationScope; +import de.monticore.cdassociation._visitor.CDAssociationVisitor2; +import de.monticore.cdbasis._ast.ASTCDClass; +import de.monticore.cdbasis._ast.ASTCDCompilationUnit; +import de.monticore.cdbasis._visitor.CDBasisVisitor2; +import de.monticore.symbols.oosymbols._symboltable.OOTypeSymbol; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.lang.model.SourceVersion; + +/** Adapts association roles to Java identifiers before fields and methods are generated. */ +public class JavaAssociationRoleNameTrafo implements CDAssociationVisitor2, CDBasisVisitor2 { + + protected final List roles = new ArrayList<>(); + protected final Map> hierarchies = new LinkedHashMap<>(); + + @Override + public void visit(ASTCDClass node) { + Set ancestors = new LinkedHashSet<>(); + collectAncestors(node.getSymbol(), ancestors); + hierarchies.put(node.getSymbol(), ancestors); + } + + protected void collectAncestors(OOTypeSymbol type, Set ancestors) { + if (ancestors.add(type)) { + type.getSuperTypesList().forEach(superType -> { + if (superType.getTypeInfo() instanceof OOTypeSymbol) { + collectAncestors((OOTypeSymbol) superType.getTypeInfo(), ancestors); + } + }); + } + } + + @Override + public void visit(ASTCDRole node) { + roles.add(node); + } + + public void transform(Collection asts) { + roles.clear(); + hierarchies.clear(); + var traverser = CD4CodeMill.inheritanceTraverser(); + traverser.add4CDBasis(this); + traverser.add4CDAssociation(this); + asts.forEach(ast -> ast.accept(traverser)); + for (ASTCDRole role : roles) { + if (!SourceVersion.isKeyword(role.getName())) { + continue; + } + var symbol = role.getSymbol(); + var scope = symbol.getEnclosingScope(); + Set occupied = new LinkedHashSet<>(); + scope.getLocalFieldSymbols().forEach(field -> occupied.add(field.getName())); + scope.getLocalCDRoleSymbols().forEach(other -> occupied.add(other.getName())); + // Reserve inherited members and members of subclasses, independent of traversal order. + hierarchies.forEach((type, ancestors) -> { + if (ancestors.stream().anyMatch(ancestor -> ancestor.getSpannedScope() == scope)) { + ancestors.forEach(ancestor -> collectNames(ancestor, occupied)); + collectNames(type, occupied); + } + }); + String name = role.getName() + "_"; + while (occupied.contains(name)) { + name += "_"; + } + // Re-index the scope as well as updating the AST and symbol used by navigable setters. + scope.remove(symbol); + role.setName(name); + symbol.setName(name); + symbol.setFullName(null); + scope.add(symbol); + } + } + + protected void collectNames(OOTypeSymbol type, Set names) { + type.getFieldList().forEach(field -> names.add(field.getName())); + if (type.getSpannedScope() instanceof ICDAssociationScope) { + ((ICDAssociationScope) type.getSpannedScope()).getLocalCDRoleSymbols().forEach(role -> names + .add(role.getName())); + } + } + +} diff --git a/cdlang/src/main/java/de/monticore/cdgen/CDGenTool.java b/cdlang/src/main/java/de/monticore/cdgen/CDGenTool.java index 6b648136c..b9bb9d20e 100644 --- a/cdlang/src/main/java/de/monticore/cdgen/CDGenTool.java +++ b/cdlang/src/main/java/de/monticore/cdgen/CDGenTool.java @@ -7,6 +7,7 @@ import de.monticore.cd.codegen.CdUtilsPrinter; import de.monticore.cd.codegen.DecoratorConfig; import de.monticore.cd.codegen.trafo.DefaultVisibilityPublicTrafo; +import de.monticore.cd.codegen.trafo.JavaAssociationRoleNameTrafo; import de.monticore.cd.codegen.trafo.TOPTrafo; import de.monticore.cd.methodtemplates.CD4C; import de.monticore.cd4analysis._util.CD4AnalysisTypeDispatcher; @@ -292,6 +293,7 @@ public GeneratorSetup newConfiguredGeneratorSetup(List additionalTemplateP public CDAssociationCreateFieldsFromAllRoles performFieldsFromRolesTrafo( Collection asts) { + new JavaAssociationRoleNameTrafo().transform(asts); CDAssociationCreateFieldsFromAllRoles roleTrafo = new CDAssociationCreateFieldsFromNavigableRoles(); final CD4CodeTraverser traverser = CD4CodeMill.inheritanceTraverser(); diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java index 0f81a741c..ebb438537 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java @@ -2,14 +2,23 @@ package de.monticore.cd.cdgen; import de.monticore.cd.codegen.DecoratorConfig; +import de.monticore.cd.codegen.trafo.JavaAssociationRoleNameTrafo; import de.monticore.cd4code.CD4CodeMill; import de.monticore.generating.GeneratorSetup; import de.monticore.generating.templateengine.GlobalExtensionManagement; import de.monticore.generating.templateengine.TemplateController; import de.monticore.generating.templateengine.TemplateHookPoint; import de.monticore.runtime.junit.MCAssertions; - +import de.se_rwth.commons.logging.Log; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaFileObject; +import javax.tools.ToolProvider; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -19,6 +28,105 @@ */ public class DefaultCD2PojoDecoratorTest extends AbstractDecoratorTest { + @Test + public void testRoleNormalizationUpdatesCachedNamesAndIsIdempotent() throws Exception { + var ast = CD4CodeMill.parser().parse_String( + "classdiagram CachedRoles { class A {} class Class {} association A -> Class; }") + .orElseThrow(); + tool.trafoBeforeSymtab(List.of(ast)); + tool.initializeSymbolTable(false); + tool.createSymbolTable(ast); + tool.completeSymbolTable(ast); + var owner = ast.getCDDefinition().getCDClassesList().get(0).getSymbol(); + var role = owner.getCDRoleList("class").get(0); + String originalName = role.getFullName(); + var trafo = new JavaAssociationRoleNameTrafo(); + trafo.transform(List.of(ast)); + trafo.transform(List.of(ast)); + Assertions.assertEquals("class_", role.getName()); + Assertions.assertEquals(originalName + "_", role.getFullName()); + Assertions.assertSame(role, owner.getCDRoleList("class_").get(0)); + Assertions.assertTrue(owner.getCDRoleList("class").isEmpty()); + MCAssertions.assertNoFindings(); + } + + @Test + public void testReservedJavaRoleNames() throws Exception { + outputDir = new File(outputDir, "reservedRoles"); + // Includes reserved but unused keywords and literals, which are not legal identifiers either. + String keywords = "abstract assert boolean break byte case catch char class const continue " + + "default do double else enum extends final finally float for goto if implements import " + + "instanceof int interface long native new package private protected public return short " + + "static strictfp super switch synchronized this throw throws transient try void volatile " + + "while true false null"; + StringBuilder model = new StringBuilder( + "classdiagram ReservedRoles { <> public class A {} "); + for (String keyword : keywords.split(" ")) { + String type = Character.toUpperCase(keyword.charAt(0)) + keyword.substring(1); + model.append("public class ").append(type).append(" {} public association A -> ").append(type) + .append("; "); + } + model.append("}"); + var ast = CD4CodeMill.parser().parse_String(model.toString()).orElseThrow(); + doTest(ast); + var owner = ast.getCDDefinition().getCDClassesList().get(0); + for (String keyword : keywords.split(" ")) { + Assertions.assertEquals(1, owner.getSymbol().getFieldList(keyword + "_").size()); + Assertions.assertEquals(1, owner.getSymbol().getCDRoleList(keyword + "_").size()); + Assertions.assertTrue(owner.getSymbol().getCDRoleList(keyword).isEmpty()); + } + MCAssertions.assertNoFindings(); + compileGeneratedSources(); + } + + @Test + public void testReservedRolesAvoidCollisions() throws Exception { + outputDir = new File(outputDir, "roleCollisions"); + var ast = CD4CodeMill.parser().parse_String(""" + classdiagram RoleCollisions { + public class Base { protected int public_; } + <> public class A extends Base { protected int public__; } + public class Child extends A { protected int public___; } + public class Public {} + public class Class {} + public class Other {} + public association A <-> Public; + public association A -> Class [0..1]; + public association A -> (public____) Other [*]; + public association A -> (class_) Other [*] {ordered}; + public association Public <-> Class [*]; + } + """).orElseThrow(); + doTest(ast); + var owner = ast.getCDDefinition().getCDClassesList().get(1); + for (String name : List.of("public_____", "class__", "public____", "class_")) { + Assertions.assertEquals(1, owner.getSymbol().getFieldList(name).size(), name); + Assertions.assertEquals(1, owner.getSymbol().getCDRoleList(name).size(), name); + } + MCAssertions.assertNoFindings(); + compileGeneratedSources(); + } + + protected void compileGeneratedSources() throws Exception { + var compiler = ToolProvider.getSystemJavaCompiler(); + Assertions.assertNotNull(compiler, "The generation regression test requires a JDK"); + var diagnostics = new DiagnosticCollector(); + Path classes = outputDir.toPath().resolve("compiled"); + Files.createDirectories(classes); + try ( + var files = Files.walk(outputDir.toPath()); + var manager = compiler.getStandardFileManager(diagnostics, null, null) + ) { + List sources = files.filter(path -> path.toString().endsWith(".java")).map(Path::toFile) + .collect(Collectors.toList()); + String loggingPath = new File(Log.class.getProtectionDomain().getCodeSource().getLocation() + .toURI()).getAbsolutePath(); + Assertions.assertTrue(compiler.getTask(null, manager, diagnostics, List.of("-d", classes + .toString(), "-classpath", loggingPath), null, manager.getJavaFileObjectsFromFiles( + sources)).call(), diagnostics.getDiagnostics().toString()); + } + } + @Test public void testAll() throws Exception { var opt = CD4CodeMill.parser().parse_String("classdiagram TestDefaultCD2Pojo {\n" From bf98b89615905ae4f447132d78cd2f8e7d52ec74 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Mon, 21 Sep 2026 10:56:34 +0200 Subject: [PATCH 09/14] Avoid unnecessary role hierarchy scans --- .../trafo/JavaAssociationRoleNameTrafo.java | 16 +++++---- .../cd/cdgen/DefaultCD2PojoDecoratorTest.java | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java index 1281c8589..6f635f3a8 100644 --- a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java +++ b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java @@ -43,20 +43,25 @@ protected void collectAncestors(OOTypeSymbol type, Set ancestors) @Override public void visit(ASTCDRole node) { - roles.add(node); + if (SourceVersion.isKeyword(node.getName())) { + roles.add(node); + } } public void transform(Collection asts) { roles.clear(); hierarchies.clear(); var traverser = CD4CodeMill.inheritanceTraverser(); - traverser.add4CDBasis(this); traverser.add4CDAssociation(this); asts.forEach(ast -> ast.accept(traverser)); + if (roles.isEmpty()) { + return; + } + // Only inspect inheritance when a role actually needs a Java identifier. + var hierarchyTraverser = CD4CodeMill.inheritanceTraverser(); + hierarchyTraverser.add4CDBasis(this); + asts.forEach(ast -> ast.accept(hierarchyTraverser)); for (ASTCDRole role : roles) { - if (!SourceVersion.isKeyword(role.getName())) { - continue; - } var symbol = role.getSymbol(); var scope = symbol.getEnclosingScope(); Set occupied = new LinkedHashSet<>(); @@ -66,7 +71,6 @@ public void transform(Collection asts) { hierarchies.forEach((type, ancestors) -> { if (ancestors.stream().anyMatch(ancestor -> ancestor.getSpannedScope() == scope)) { ancestors.forEach(ancestor -> collectNames(ancestor, occupied)); - collectNames(type, occupied); } }); String name = role.getName() + "_"; diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java index ebb438537..2acab9c0e 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java @@ -21,6 +21,8 @@ import javax.tools.ToolProvider; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Tests the decorators configured by CD2Java. The @@ -28,6 +30,40 @@ */ public class DefaultCD2PojoDecoratorTest extends AbstractDecoratorTest { + @ParameterizedTest + @ValueSource(booleans = { false, true }) + public void testReservedRolesAcrossCDs(boolean reverseOrder) throws Exception { + var base = CD4CodeMill.parser().parse_String( + "classdiagram BaseModel { public class Base { protected int class_; } }").orElseThrow(); + var target = CD4CodeMill.parser().parse_String(""" + classdiagram TargetModel { + public class A extends BaseModel.Base {} + public class Class {} + public association A -> Class; + } + """).orElseThrow(); + var child = CD4CodeMill.parser().parse_String( + "classdiagram ChildModel { public class Child extends TargetModel.A { protected int class__; } }") + .orElseThrow(); + var asts = reverseOrder ? List.of(child, target, base) : List.of(base, target, child); + tool.trafoBeforeSymtab(asts); + tool.initializeSymbolTable(false); + asts.forEach(ast -> tool.createSymbolTable(ast)); + asts.forEach(tool::completeSymbolTable); + outputDir = new File(outputDir, "crossCDRoles" + reverseOrder); + var glex = new GlobalExtensionManagement(); + var setup = tool.newConfiguredGeneratorSetup(getAdditionalTemplatesPath(), getHandWrittenPath(), + outputDir.getAbsolutePath(), glex); + tool.decorateAndGenerate(glex, config -> initializeDecConf(glex, config, setup), setup, + () -> tool.initDecoratedGlobalScope(false), decorated -> {}, asts); + var owner = target.getCDDefinition().getCDClassesList().get(0).getSymbol(); + Assertions.assertEquals(1, owner.getFieldList("class___").size()); + Assertions.assertEquals(1, owner.getCDRoleList("class___").size()); + Assertions.assertTrue(owner.getCDRoleList("class").isEmpty()); + MCAssertions.assertNoFindings(); + compileGeneratedSources(); + } + @Test public void testRoleNormalizationUpdatesCachedNamesAndIsIdempotent() throws Exception { var ast = CD4CodeMill.parser().parse_String( From 7b046e789ebe72f4003fc17e1b39d32a42d47184 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Tue, 22 Sep 2026 10:59:16 +0200 Subject: [PATCH 10/14] Fix keyword role CI tests --- .../cd/cdgen/DefaultCD2PojoDecoratorTest.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java index 2acab9c0e..a516de031 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java @@ -33,21 +33,23 @@ public class DefaultCD2PojoDecoratorTest extends AbstractDecoratorTest { @ParameterizedTest @ValueSource(booleans = { false, true }) public void testReservedRolesAcrossCDs(boolean reverseOrder) throws Exception { - var base = CD4CodeMill.parser().parse_String( - "classdiagram BaseModel { public class Base { protected int class_; } }").orElseThrow(); + String packageName = "crosscdroles" + reverseOrder; + var base = CD4CodeMill.parser().parse_String("package " + packageName + + "; classdiagram BaseModel { public class Base { protected int class_; } }").orElseThrow(); var target = CD4CodeMill.parser().parse_String(""" + package %s; classdiagram TargetModel { - public class A extends BaseModel.Base {} + public class A extends %s.BaseModel.Base {} public class Class {} public association A -> Class; } - """).orElseThrow(); - var child = CD4CodeMill.parser().parse_String( - "classdiagram ChildModel { public class Child extends TargetModel.A { protected int class__; } }") - .orElseThrow(); + """.formatted(packageName, packageName)).orElseThrow(); + var child = CD4CodeMill.parser().parse_String("package " + packageName + + "; classdiagram ChildModel { public class Child extends " + packageName + + ".TargetModel.A { protected int class__; } }").orElseThrow(); var asts = reverseOrder ? List.of(child, target, base) : List.of(base, target, child); tool.trafoBeforeSymtab(asts); - tool.initializeSymbolTable(false); + tool.initializeSymbolTable(false, true); asts.forEach(ast -> tool.createSymbolTable(ast)); asts.forEach(tool::completeSymbolTable); outputDir = new File(outputDir, "crossCDRoles" + reverseOrder); @@ -70,7 +72,7 @@ public void testRoleNormalizationUpdatesCachedNamesAndIsIdempotent() throws Exce "classdiagram CachedRoles { class A {} class Class {} association A -> Class; }") .orElseThrow(); tool.trafoBeforeSymtab(List.of(ast)); - tool.initializeSymbolTable(false); + tool.initializeSymbolTable(false, true); tool.createSymbolTable(ast); tool.completeSymbolTable(ast); var owner = ast.getCDDefinition().getCDClassesList().get(0).getSymbol(); From 84ca1365c530e01098a0dd08e92478f3738283d6 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Wed, 23 Sep 2026 12:23:32 +0200 Subject: [PATCH 11/14] Fix Gradle keyword role generation --- .../trafo/JavaAssociationRoleNameTrafo.java | 14 ++++++-- .../cocos/CDAssociationReferenceUnique.java | 32 +++++++++---------- .../cd/cdgen/DefaultCD2PojoDecoratorTest.java | 5 ++- .../cdgradle-it/src/main/cds/MyCD.cd | 4 +++ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java index 6f635f3a8..919f17de1 100644 --- a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java +++ b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java @@ -16,11 +16,21 @@ import java.util.List; import java.util.Map; import java.util.Set; -import javax.lang.model.SourceVersion; /** Adapts association roles to Java identifiers before fields and methods are generated. */ public class JavaAssociationRoleNameTrafo implements CDAssociationVisitor2, CDBasisVisitor2 { + // Keep this check in java.base: Gradle's isolated generator classloader cannot load + // javax.lang.model.SourceVersion from the java.compiler module. + // Includes reserved keywords, the underscore, and literals; contextual keywords are legal fields. + protected static final Set JAVA_RESERVED_NAMES = Set.of("abstract", "assert", "boolean", + "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", + "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", + "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", + "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", + "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while", + "_", "true", "false", "null"); + protected final List roles = new ArrayList<>(); protected final Map> hierarchies = new LinkedHashMap<>(); @@ -43,7 +53,7 @@ protected void collectAncestors(OOTypeSymbol type, Set ancestors) @Override public void visit(ASTCDRole node) { - if (SourceVersion.isKeyword(node.getName())) { + if (JAVA_RESERVED_NAMES.contains(node.getName())) { roles.add(node); } } diff --git a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java index c1d4af6ec..0756d3f0a 100644 --- a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java +++ b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java @@ -13,11 +13,11 @@ * Checks that generated association references are unique. */ public class CDAssociationReferenceUnique extends CDAssociationUniqueInHierarchy { - + @Override public void check(ASTCDDefinition node) { List alreadyChecked = new ArrayList<>(); - + // we check for each pair of associations for (ASTCDAssociation assoc2 : node.getCDAssociationsList()) { for (ASTCDAssociation assoc1 : alreadyChecked) { @@ -29,11 +29,11 @@ public void check(ASTCDDefinition node) { } } } - + alreadyChecked.add(assoc2); } } - + @Override protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, ASTCDAssociation assoc1) { @@ -42,14 +42,14 @@ protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, } super.checkRef(node, type1, type2, assoc1); } - + protected List getAssociationReferences(ASTCDAssociation assoc) { List references = new ArrayList<>(); - + boolean navigableLeft = assoc.getCDAssocDir().isDefinitiveNavigableLeft(); boolean navigableRight = assoc.getCDAssocDir().isDefinitiveNavigableRight(); boolean undirected = !navigableLeft && !navigableRight; - + if (navigableRight || undirected) { references.add(new AssociationReference(findTypeByFullName(assoc, assoc.getLeftQualifiedName() .getQName()), deriveReferenceName(assoc, AssocSide.RIGHT))); @@ -58,10 +58,10 @@ protected List getAssociationReferences(ASTCDAssociation a references.add(new AssociationReference(findTypeByFullName(assoc, assoc .getRightQualifiedName().getQName()), deriveReferenceName(assoc, AssocSide.LEFT))); } - + return references; } - + protected String deriveReferenceName(ASTCDAssociation assoc, AssocSide side) { ASTCDAssocSide assocSide; if (side.equals(AssocSide.LEFT)) { @@ -81,22 +81,22 @@ else if (assocSide.isPresentCDRole()) { .getBaseName()); } } - + protected enum AssocSide { LEFT, RIGHT; } - + protected static class AssociationReference { - + protected final ASTCDType sourceType; - + protected final String name; - + protected AssociationReference(ASTCDType sourceType, String name) { this.sourceType = sourceType; this.name = name; } - + } - + } diff --git a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java index a516de031..b1f2a6692 100644 --- a/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java +++ b/cdlang/src/test/java/de/monticore/cd/cdgen/DefaultCD2PojoDecoratorTest.java @@ -104,10 +104,13 @@ public void testReservedJavaRoleNames() throws Exception { model.append("public class ").append(type).append(" {} public association A -> ").append(type) .append("; "); } - model.append("}"); + model.append("public class Underscore {} public association A -> (_) Underscore; }"); var ast = CD4CodeMill.parser().parse_String(model.toString()).orElseThrow(); doTest(ast); var owner = ast.getCDDefinition().getCDClassesList().get(0); + Assertions.assertEquals(1, owner.getSymbol().getFieldList("__").size()); + Assertions.assertEquals(1, owner.getSymbol().getCDRoleList("__").size()); + Assertions.assertTrue(owner.getSymbol().getCDRoleList("_").isEmpty()); for (String keyword : keywords.split(" ")) { Assertions.assertEquals(1, owner.getSymbol().getFieldList(keyword + "_").size()); Assertions.assertEquals(1, owner.getSymbol().getCDRoleList(keyword + "_").size()); diff --git a/cdtool/cdgradle/src/test/resources/cdgradle-it/src/main/cds/MyCD.cd b/cdtool/cdgradle/src/test/resources/cdgradle-it/src/main/cds/MyCD.cd index 24c8339ea..497167162 100644 --- a/cdtool/cdgradle/src/test/resources/cdgradle-it/src/main/cds/MyCD.cd +++ b/cdtool/cdgradle/src/test/resources/cdgradle-it/src/main/cds/MyCD.cd @@ -23,6 +23,10 @@ classdiagram MyCD { association A <-> B; + // Exercise Java keyword normalization in the isolated Gradle generator classloader. + public class Public {} + public association A -> Public; + // Test BiDirectional association generation class A1; class A2; From 96061a418c5f9b8be3063dd6279de8e99cad32bb Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Wed, 23 Sep 2026 22:43:59 +0200 Subject: [PATCH 12/14] Clarify keyword role comment --- .../cd/codegen/trafo/JavaAssociationRoleNameTrafo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java index 919f17de1..184ca61a4 100644 --- a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java +++ b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java @@ -20,8 +20,8 @@ /** Adapts association roles to Java identifiers before fields and methods are generated. */ public class JavaAssociationRoleNameTrafo implements CDAssociationVisitor2, CDBasisVisitor2 { - // Keep this check in java.base: Gradle's isolated generator classloader cannot load - // javax.lang.model.SourceVersion from the java.compiler module. + // Avoid SourceVersion because Gradle's isolated generator classloader does not expose + // the java.compiler module. // Includes reserved keywords, the underscore, and literals; contextual keywords are legal fields. protected static final Set JAVA_RESERVED_NAMES = Set.of("abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", From 72a0108281052d4c1d662cef4c9b2ef3608fd64d Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Mon, 28 Sep 2026 13:47:41 +0200 Subject: [PATCH 13/14] Fix association reference type lookup --- .../cocos/CDAssociationReferenceUnique.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java index 0756d3f0a..dedd5ff8a 100644 --- a/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java +++ b/cdlang/src/main/java/de/monticore/cdgen/cocos/CDAssociationReferenceUnique.java @@ -4,7 +4,7 @@ import de.monticore.cdassociation._ast.ASTCDAssocSide; import de.monticore.cdassociation._ast.ASTCDAssociation; import de.monticore.cdbasis._ast.ASTCDDefinition; -import de.monticore.cdbasis._ast.ASTCDType; +import de.monticore.symbols.basicsymbols._symboltable.TypeSymbol; import org.apache.commons.lang3.StringUtils; import java.util.*; @@ -35,7 +35,7 @@ public void check(ASTCDDefinition node) { } @Override - protected void checkRef(ASTCDDefinition node, ASTCDType type1, ASTCDType type2, + protected void checkRef(ASTCDDefinition node, TypeSymbol type1, TypeSymbol type2, ASTCDAssociation assoc1) { if (type1 == null || type2 == null) { return; @@ -51,12 +51,12 @@ protected List getAssociationReferences(ASTCDAssociation a boolean undirected = !navigableLeft && !navigableRight; if (navigableRight || undirected) { - references.add(new AssociationReference(findTypeByFullName(assoc, assoc.getLeftQualifiedName() - .getQName()), deriveReferenceName(assoc, AssocSide.RIGHT))); + references.add(new AssociationReference(findTypeByFullName(assoc.getLeftQualifiedName()), + deriveReferenceName(assoc, AssocSide.RIGHT))); } if (navigableLeft || undirected) { - references.add(new AssociationReference(findTypeByFullName(assoc, assoc - .getRightQualifiedName().getQName()), deriveReferenceName(assoc, AssocSide.LEFT))); + references.add(new AssociationReference(findTypeByFullName(assoc.getRightQualifiedName()), + deriveReferenceName(assoc, AssocSide.LEFT))); } return references; @@ -88,11 +88,11 @@ protected enum AssocSide { protected static class AssociationReference { - protected final ASTCDType sourceType; + protected final TypeSymbol sourceType; protected final String name; - protected AssociationReference(ASTCDType sourceType, String name) { + protected AssociationReference(TypeSymbol sourceType, String name) { this.sourceType = sourceType; this.name = name; } From fe5b1e7e9b0938fc960d615f6c5bf0af6dccf880 Mon Sep 17 00:00:00 2001 From: Vanol Nguemo Tadoum Date: Thu, 1 Oct 2026 14:05:27 +0200 Subject: [PATCH 14/14] Use pattern matching for super types --- .../cd/codegen/trafo/JavaAssociationRoleNameTrafo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java index 184ca61a4..876d0ead4 100644 --- a/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java +++ b/cdlang/src/main/java/de/monticore/cd/codegen/trafo/JavaAssociationRoleNameTrafo.java @@ -44,8 +44,8 @@ public void visit(ASTCDClass node) { protected void collectAncestors(OOTypeSymbol type, Set ancestors) { if (ancestors.add(type)) { type.getSuperTypesList().forEach(superType -> { - if (superType.getTypeInfo() instanceof OOTypeSymbol) { - collectAncestors((OOTypeSymbol) superType.getTypeInfo(), ancestors); + if (superType.getTypeInfo() instanceof OOTypeSymbol ooSuperTypeInfo) { + collectAncestors(ooSuperTypeInfo, ancestors); } }); }