diff --git a/cdlang/src/main/java/de/monticore/cdassociation/_symboltable/CDAssociationSymbolTableCompleter.java b/cdlang/src/main/java/de/monticore/cdassociation/_symboltable/CDAssociationSymbolTableCompleter.java index df5f1d757..ec1e7b6fd 100644 --- a/cdlang/src/main/java/de/monticore/cdassociation/_symboltable/CDAssociationSymbolTableCompleter.java +++ b/cdlang/src/main/java/de/monticore/cdassociation/_symboltable/CDAssociationSymbolTableCompleter.java @@ -146,12 +146,13 @@ public void endVisit(ASTCDAssociation node) { public void setupModifiers(ASTModifier assocSideModifier, ASTModifier assocModifier, CDRoleSymbol roleSymbol) { - roleSymbol.setIsPublic(assocSideModifier.isPublic() || assocModifier.isPublic()); - roleSymbol.setIsPrivate(assocSideModifier.isPrivate() || assocModifier.isPrivate()); - roleSymbol.setIsProtected(assocSideModifier.isProtected() || assocModifier.isProtected()); + roleSymbol.setIsPublic(assocSideModifier.isPublic()); + roleSymbol.setIsPrivate(assocSideModifier.isPrivate()); + roleSymbol.setIsProtected(assocSideModifier.isProtected()); roleSymbol.setIsStatic(assocSideModifier.isStatic() || assocModifier.isStatic()); roleSymbol.setIsFinal(assocSideModifier.isFinal() || assocModifier.isFinal()); roleSymbol.setIsDerived(assocSideModifier.isDerived() || assocModifier.isDerived()); + roleSymbol.setIsReadOnly(assocSideModifier.isReadonly() || assocModifier.isReadonly()); } public static void addRoleToTheirType(CDRoleSymbol symbol, TypeSymbol otherType) { diff --git a/cdlang/src/main/java/de/monticore/cdassociation/cocos/CDAssociationCoCos.java b/cdlang/src/main/java/de/monticore/cdassociation/cocos/CDAssociationCoCos.java index 1d93b5339..5e58ea795 100644 --- a/cdlang/src/main/java/de/monticore/cdassociation/cocos/CDAssociationCoCos.java +++ b/cdlang/src/main/java/de/monticore/cdassociation/cocos/CDAssociationCoCos.java @@ -28,6 +28,8 @@ protected void addEbnfCoCos(CDAssociationCoCoChecker checker) { checker.addCoCo(new CDAssociationOrderedCardinalityGreaterOne()); checker.addCoCo(new CDAssociationRoleNameLowerCase()); checker.addCoCo(new CDAssociationRoleNameNoConflictWithLocalAttribute()); + checker.addCoCo(new CDAssociationModifierCoCo()); + checker.addCoCo(new CDAssociationJavaClassTypeCoCo()); } } diff --git a/cdlang/src/main/java/de/monticore/cdassociation/cocos/ebnf/CDAssociationJavaClassTypeCoCo.java b/cdlang/src/main/java/de/monticore/cdassociation/cocos/ebnf/CDAssociationJavaClassTypeCoCo.java new file mode 100644 index 000000000..c9ca16f02 --- /dev/null +++ b/cdlang/src/main/java/de/monticore/cdassociation/cocos/ebnf/CDAssociationJavaClassTypeCoCo.java @@ -0,0 +1,45 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.cdassociation.cocos.ebnf; + +import de.monticore.cdassociation._ast.ASTCDAssociation; +import de.monticore.cdassociation._cocos.CDAssociationASTCDAssociationCoCo; +import de.monticore.cdassociation._symboltable.ICDAssociationScope; +import de.monticore.cdbasis._symboltable.CDTypeSymbol; +import de.monticore.symbols.basicsymbols._symboltable.TypeSymbol; +import de.monticore.types.mcbasictypes._ast.ASTMCQualifiedType; +import de.se_rwth.commons.logging.Log; + +public class CDAssociationJavaClassTypeCoCo implements CDAssociationASTCDAssociationCoCo { + + @Override + public void check(ASTCDAssociation node) { + check(node, node.getLeft().getMCQualifiedType(), "left"); + check(node, node.getRight().getMCQualifiedType(), "right"); + } + + protected void check(ASTCDAssociation a, ASTMCQualifiedType qualifiedType, String sideName) { + + if (qualifiedType == null || a == null) { + return; + } + + ICDAssociationScope scope = a.getEnclosingScope(); + String qName = qualifiedType.getMCQualifiedName().getQName(); + + var optTypeSymbol = scope.resolveType(qName); + + if (optTypeSymbol.isPresent()) { + TypeSymbol typeSymbol = optTypeSymbol.get(); + + if (!(typeSymbol instanceof CDTypeSymbol)) { + String assocName = a.isPresentName() ? a.getName() : "unnamed"; + + Log.error(String.format( + "0xCDA64: Java class '%s' cannot be used on the %s side of association '%s'.", qName, + sideName, assocName), a.get_SourcePositionStart()); + + } + } + } + +} diff --git a/cdlang/src/main/java/de/monticore/cdassociation/cocos/ebnf/CDAssociationModifierCoCo.java b/cdlang/src/main/java/de/monticore/cdassociation/cocos/ebnf/CDAssociationModifierCoCo.java new file mode 100644 index 000000000..d698f9175 --- /dev/null +++ b/cdlang/src/main/java/de/monticore/cdassociation/cocos/ebnf/CDAssociationModifierCoCo.java @@ -0,0 +1,111 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.cdassociation.cocos.ebnf; + +import de.monticore.cdassociation._ast.ASTCDAssociation; +import de.monticore.cdassociation._cocos.CDAssociationASTCDAssociationCoCo; +import de.monticore.cdassociation._symboltable.ICDAssociationScope; +import de.monticore.cdbasis._symboltable.CDTypeSymbol; +import de.monticore.symbols.basicsymbols._symboltable.TypeSymbol; +import de.monticore.umlmodifier._ast.ASTModifier; +import de.se_rwth.commons.logging.Log; + +import java.util.Optional; +import java.util.function.Predicate; + +public class CDAssociationModifierCoCo implements CDAssociationASTCDAssociationCoCo { + + @Override + public void check(ASTCDAssociation a) { + + String assocName = a.isPresentName() ? a.getName() : "unnamed"; + + // Check for conflicting visibility modifiers + boolean hasMain = hasVisibility(a.getModifier()); + boolean hasRoleVisibility = hasVisibility(a.getLeft().getModifier()) || hasVisibility(a + .getRight().getModifier()); + + if (hasMain && hasRoleVisibility) { + Log.warn(String.format( + "The association '%s' cannot combine a main visibility modifier with individual role visibility modifiers.", + assocName), a.get_SourcePositionStart()); + } + + //----------------------------------check the rest modifiers----------------------------------------------------------- + + //// Ensure that the association does not connect static classes + ICDAssociationScope scope = a.getEnclosingScope(); + + boolean staticClassLeft = isStaticType(scope.resolveType(a.getLeft().getName())); + boolean staticClassRight = isStaticType(scope.resolveType(a.getRight().getName())); + + if (staticClassLeft || staticClassRight) { + Log.error(String.format("0xCDA66: The association '%s' cannot connect static classes.", + assocName), a.get_SourcePositionStart()); + } + + //// Ensure that neither the association nor its roles use forbidden modifiers (static, abstract, local) + checkForbiddenModifier(a, ASTModifier::isStatic, "0xCDA61", String.format( + "The association '%s' (or its roles) must not use static modifiers.", assocName)); + checkForbiddenModifier(a, ASTModifier::isAbstract, "0xCDA67", String.format( + "The association '%s' (or its roles) must not use an abstract modifier.", assocName)); + checkForbiddenModifier(a, ASTModifier::isLocal, "0xCDA68", String.format( + "The association '%s' (or its roles) must not use a local modifier.", assocName)); + + } + + /** + * Checks whether the given modifier specifies an explicit visibility + * (i.e., public, protected, or private). + * + * @param modifier the AST modifier to check (can be null) + * @return true if a public, protected, or private modifier is present, false otherwise + */ + private boolean hasVisibility(ASTModifier modifier) { + return modifier != null && (modifier.isPublic() || modifier.isProtected() || modifier + .isPrivate()); + } + + /** + * Safely checks a boolean flag on an ASTModifier, handling null values. + * + * @param modifier the AST modifier to check (can be null) + * @param check a predicate representing the flag to check + * @return true if the modifier is present and the check evaluates to true, false otherwise + */ + private boolean hasModifier(ASTModifier modifier, Predicate check) { + return modifier != null && check.test(modifier); + } + + /** + * Checks whether the given type symbol represents a static class. + * + * @param optSymbol an optional type symbol resolved from the scope + * @return true if the symbol is a static class, false otherwise + */ + private boolean isStaticType(Optional optSymbol) { + if (optSymbol.isPresent() && optSymbol.get() instanceof CDTypeSymbol typeSymbol) { + return typeSymbol.isPresentAstNode() && typeSymbol.getAstNode().getModifier().isStatic(); + } + return false; + } + + /** + * Checks whether a forbidden modifier is present on the association itself or its roles, + * and logs an error if the check evaluates to true. + * + * @param a the association AST node to check + * @param check a predicate representing the modifier flag to check + * @param errorCode the error code to log + * @param errorMessage the error message to log + */ + private void checkForbiddenModifier(ASTCDAssociation a, Predicate check, + String errorCode, String errorMessage) { + boolean main = hasModifier(a.getModifier(), check); + boolean left = hasModifier(a.getLeft().getModifier(), check); + boolean right = hasModifier(a.getRight().getModifier(), check); + if (main || left || right) { + Log.error(String.format("%s: %s", errorCode, errorMessage), a.get_SourcePositionStart()); + } + } + +} diff --git a/cdlang/src/test/java/de/monticore/testcdassociation/cocos/CDAssociationLeftAndRightJavaClassInvalidTest.java b/cdlang/src/test/java/de/monticore/testcdassociation/cocos/CDAssociationLeftAndRightJavaClassInvalidTest.java new file mode 100644 index 000000000..5f190f517 --- /dev/null +++ b/cdlang/src/test/java/de/monticore/testcdassociation/cocos/CDAssociationLeftAndRightJavaClassInvalidTest.java @@ -0,0 +1,50 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.testcdassociation.cocos; + +import de.monticore.cdassociation.cocos.ebnf.CDAssociationJavaClassTypeCoCo; +import de.monticore.cdbasis._ast.ASTCDCompilationUnit; +import de.monticore.testcdassociation.CDAssociationTestBasis; +import de.se_rwth.commons.logging.Log; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class CDAssociationLeftAndRightJavaClassInvalidTest extends CDAssociationTestBasis { + + @Test + public void testValid() throws IOException { + coCoChecker.addCoCo(new CDAssociationJavaClassTypeCoCo()); + final Optional optAST = p.parse(getFilePath( + "cdassociation/cocos/CDAssociationLeftAndRightJavaClassValid.cd")); + assertTrue(optAST.isPresent()); + + final ASTCDCompilationUnit ast = optAST.get(); + Log.getFindings().clear(); + createSymTab(ast); + completeSymTab(ast); + coCoChecker.checkAll(ast); + assertTrue(Log.getFindings().isEmpty()); + } + + @Test + public void testInvalid() throws IOException { + + coCoChecker.addCoCo(new CDAssociationJavaClassTypeCoCo()); + final Optional optAST = p.parse(getFilePath( + "cdassociation/cocos/CDAssociationLeftAndRightJavaClassInvalid.cd")); + assertTrue(optAST.isPresent()); + + final ASTCDCompilationUnit ast = optAST.get(); + Log.getFindings().clear(); + createSymTab(ast); + completeSymTab(ast); + coCoChecker.checkAll(ast); + assertTrue(Log.getFindings().get(0).getMsg().startsWith("0xCDA64")); + assertEquals(4, Log.getFindings().size()); + Log.clearFindings(); + } + +} diff --git a/cdlang/src/test/java/de/monticore/testcdassociation/cocos/CDAssociationModifierCoCoTest.java b/cdlang/src/test/java/de/monticore/testcdassociation/cocos/CDAssociationModifierCoCoTest.java new file mode 100644 index 000000000..d926c355d --- /dev/null +++ b/cdlang/src/test/java/de/monticore/testcdassociation/cocos/CDAssociationModifierCoCoTest.java @@ -0,0 +1,60 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.testcdassociation.cocos; + +import de.monticore.cdassociation.cocos.ebnf.CDAssociationModifierCoCo; +import de.monticore.cdassociation.cocos.ebnf.CDAssociationNameUnique; +import de.monticore.cdbasis._ast.ASTCDCompilationUnit; +import de.monticore.testcdassociation.CDAssociationTestBasis; +import de.se_rwth.commons.logging.Log; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CDAssociationModifierCoCoTest extends CDAssociationTestBasis { + + @Test + public void testValid() throws IOException { + coCoChecker.addCoCo(new CDAssociationNameUnique()); + final Optional optAST = p.parse(getFilePath( + "cdassociation/cocos/Valid.cd")); + assertTrue(optAST.isPresent()); + final ASTCDCompilationUnit ast = optAST.get(); + Log.getFindings().clear(); + coCoChecker.checkAll(ast); + assertTrue(Log.getFindings().isEmpty()); + } + + //visibility-modifiers tests + @Test + public void testModifiersValid() throws IOException { + coCoChecker.addCoCo(new CDAssociationModifierCoCo()); + final Optional optAST = p.parse(getFilePath( + "cdassociation/cocos/CDAssociationModifierCoCoValid.cd")); + assertTrue(optAST.isPresent()); + final ASTCDCompilationUnit ast = optAST.get(); + Log.getFindings().clear(); + createSymTab(ast); + coCoChecker.checkAll(ast); + assertTrue(Log.getFindings().isEmpty()); + } + + // static/abstract/local modifier test + @Test + public void testModifierInvalid() throws IOException { + coCoChecker.addCoCo(new CDAssociationModifierCoCo()); + final Optional optAST = p.parse(getFilePath( + "cdassociation/cocos/CDAssociationModifierCoCoInvalid.cd")); + assertTrue(optAST.isPresent()); + final ASTCDCompilationUnit ast = optAST.get(); + Log.getFindings().clear(); + createSymTab(ast); + coCoChecker.checkAll(ast); + assertEquals(13, Log.getFindings().size()); + Log.getFindings().clear(); + } + +} diff --git a/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationLeftAndRightJavaClassInvalid.cd b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationLeftAndRightJavaClassInvalid.cd new file mode 100644 index 000000000..6b50498c7 --- /dev/null +++ b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationLeftAndRightJavaClassInvalid.cd @@ -0,0 +1,16 @@ +/* (c) https://github.com/MontiCore/monticore */ + +classdiagram CDAssociationLeftAndRightJavaClassInvalid{ + + class A{ + } + + //left side is an external java class + association leftJava java.lang.String -> A; + + // right side is an external java class + association rightJava A -> java.lang.Integer; + + // both sides are external java classes + association bothJava java.lang.Boolean -> java.lang.Long; +} diff --git a/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationLeftAndRightJavaClassValid.cd b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationLeftAndRightJavaClassValid.cd new file mode 100644 index 000000000..ee2dc6096 --- /dev/null +++ b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationLeftAndRightJavaClassValid.cd @@ -0,0 +1,23 @@ +/* (c) https://github.com/MontiCore/monticore */ + +classdiagram CDAssociationLeftAndRightJavaClassInvalid{ + + public class A{ + java.lang.String attributeA1; + java.lang.Integer attributeA2; + } + + private abstract class B{ + readonly int attributeB1; + final java.lang.String attributeB2; + } + + protected static class C { + } + + association abstract B -> A; + association [0..2] A -> B; + + association A -> C; + +} diff --git a/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationModifierCoCoInvalid.cd b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationModifierCoCoInvalid.cd new file mode 100644 index 000000000..843fe7514 --- /dev/null +++ b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationModifierCoCoInvalid.cd @@ -0,0 +1,35 @@ +/* (c) https://github.com/MontiCore/monticore */ + +classdiagram CDAssociationModifierCoCoInvalid{ + + package p1 { + public class A { + int foo; + int bar; + int field; + } + class B extends A; + } + package p2 { + class C; + static class D; + } + + public association visibilityAssocLeft private p1.A -> p2.C; + private association visibilityAssocRight p1.A -> p2.C public; + + static association staticAssoc p1.A -> p2.C; + public association leftRoleStatic static p1.A -> p2.C; + private association rightRoleStatic [0..1] p1.A -> p2.C [*] static; + + local association localAssoc p1.A -> p2.C; + public association leftLocalRole local p1.A -> p2.C; + private association rightRoleLocal [0..1] p1.A -> p2.C [*] local; + + abstract association abstractAssoc p1.A -> p2.C; + public association leftRoleAbstract abstract p1.A -> p2.C; + private association rightRoleAbstract [0..1] p1.A -> p2.C [*] abstract; + + association staticClassLeft p2.D -> p1.A; + association staticClassRight p2.C -> p2.D; +} diff --git a/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationModifierCoCoValid.cd b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationModifierCoCoValid.cd new file mode 100644 index 000000000..b0ce79183 --- /dev/null +++ b/cdlang/src/test/resources/de/monticore/cdassociation/cocos/CDAssociationModifierCoCoValid.cd @@ -0,0 +1,22 @@ +/* (c) https://github.com/MontiCore/monticore */ + +classdiagram CDAssociationModifierCoCoValid{ + + package p1 { + public class A { + int foo; + String field; + } + } + package p2 { + class B; + } + + public association assocVisibility p1.A -> p2.B; + association leftVisibility private p1.A -> p2.B; + association rightVisibility p1.A -> p2.B public; + + public association finalRole final p2.B -> p1.A final; + private association derivedRoles derived p2.B -> p1.A derived; + protected association readOnlyRoles readonly p1.A -> p2.B readonly; +}