diff --git a/language/src/main/java/de/monticore/lang/sysmlv2/cocos/MKPX_CoCo4.java b/language/src/main/java/de/monticore/lang/sysmlv2/cocos/MKPX_CoCo4.java new file mode 100644 index 00000000..ff9a357b --- /dev/null +++ b/language/src/main/java/de/monticore/lang/sysmlv2/cocos/MKPX_CoCo4.java @@ -0,0 +1,96 @@ +/* (c) https://github.com/MontiCore/monticore */ +package de.monticore.lang.sysmlv2.cocos; + +import de.monticore.lang.sysmlbasis._ast.ASTEndpoint; +import de.monticore.lang.sysmlparts._ast.ASTConnectionUsage; +import de.monticore.lang.sysmlparts._cocos.SysMLPartsASTConnectionUsageCoCo; +import de.monticore.lang.sysmlparts._symboltable.ISysMLPartsScope; +import de.monticore.lang.sysmlparts._symboltable.PartDefSymbol; +import de.monticore.lang.sysmlparts._symboltable.PartUsageSymbol; +import de.monticore.symboltable.modifiers.AccessModifier; +import de.se_rwth.commons.logging.Log; + +import java.util.List; +import java.util.Optional; + +/** + * MKPX_CoCo4: + * In einer Verbindung "connect a.b to c.d" muss jeder verwendete (qualifizierte) Portname existieren. + */ +public class MKPX_CoCo4 implements SysMLPartsASTConnectionUsageCoCo { + + @Override + public void check(ASTConnectionUsage node) { + if (!node.isPresentSrc() || !node.isPresentTgt()) { + return; // keine Verbindung + } + + ISysMLPartsScope scope = node.getEnclosingScope(); + + checkEndpoint(scope, node.getSrc(), node); + checkEndpoint(scope, node.getTgt(), node); + } + + protected void checkEndpoint(ISysMLPartsScope scope, + ASTEndpoint endpoint, + ASTConnectionUsage conn) { + String qname = endpointQName(endpoint); + if (qname.isEmpty()) { + return; + } + + String[] parts = qname.split("\\."); + + // Qualifizierter Name: a.b -> resolve a (PartUsage), dann b in PartDef + if (parts.length >= 2) { + String partName = parts[0]; + String portName = parts[parts.length - 1]; + + Optional partOpt = scope.resolvePartUsageLocally(partName); + + if (partOpt.isEmpty()) { + // Existenz der Subkomponente wird bereits in MKPX_CoCo3 geprüft. + return; + } + + var partDefOpt = partOpt.get().getPartDef(); + if (partDefOpt.isEmpty()) { + // CoCo3 + return; + } + + PartDefSymbol partDef = partDefOpt.get(); + boolean portExistsInDef = partDef.getSpannedScope() + .resolvePortUsageLocallyMany(false, portName, AccessModifier.ALL_INCLUSION, p -> true) + .size() == 1; + + if (!portExistsInDef) { + Log.error( + "0xMKPX04 The port '" + portName + "' does not exist in the definition of subcomponent '" + + partName + "'.", + conn.get_SourcePositionStart(), + conn.get_SourcePositionEnd() + ); + } + } + else { + // Unqualifiziert: Port der Oberkomponente muss lokal existieren + String portName = parts[0]; + List localPorts = scope.resolvePortUsageLocallyMany( + false, portName, AccessModifier.ALL_INCLUSION, p -> true); + if (localPorts.isEmpty()) { + Log.error( + "0xMKPX04 The port used in 'connect' '" + portName + + "' does not exist in the parent component.", + conn.get_SourcePositionStart(), + conn.get_SourcePositionEnd() + ); + } + } + } + + /** Liefert den qualifizierten Namen des Endpunkts als String. */ + protected String endpointQName(ASTEndpoint ep) { + return ep.getMCQualifiedName() != null ? ep.getMCQualifiedName().toString() : ""; + } +} diff --git a/language/src/test/java/cocos/MKPXCoCo4Test.java b/language/src/test/java/cocos/MKPXCoCo4Test.java new file mode 100644 index 00000000..180aa5bd --- /dev/null +++ b/language/src/test/java/cocos/MKPXCoCo4Test.java @@ -0,0 +1,84 @@ +/* (c) https://github.com/MontiCore/monticore */ +package cocos; + +import de.monticore.lang.sysmlparts._cocos.SysMLPartsASTConnectionUsageCoCo; +import de.monticore.lang.sysmlparts._cocos.SysMLPartsASTPartDefCoCo; +import de.monticore.lang.sysmlparts._cocos.SysMLPartsASTPartUsageCoCo; +import de.monticore.lang.sysmlv2.SysMLv2Mill; +import de.monticore.lang.sysmlv2._ast.ASTSysMLModel; +import de.monticore.lang.sysmlv2._cocos.SysMLv2CoCoChecker; +import de.monticore.lang.sysmlv2._parser.SysMLv2Parser; +import de.monticore.lang.sysmlv2.cocos.MKPX_CoCo4; +import de.se_rwth.commons.logging.Log; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MKPXCoCo4Test { + + private static final String MODEL_PATH = "src/test/resources/parser"; + + private SysMLv2Parser parser = SysMLv2Mill.parser(); + + @BeforeAll + public static void init() { + Log.init(); + SysMLv2Mill.init(); + } + + @BeforeEach + public void reset() { + SysMLv2Mill.globalScope().clear(); + Log.getFindings().clear(); + Log.enableFailQuick(true); + } + + @Nested + public class MKPXCoCo4Tests { + @Test + public void testValid() throws IOException { + String validModel = + "part def A { port p: int; }" + + "part def B { port q: ~int; }" + + "part def System {" + + "part a: A;" + + "part b: B;" + + "connect a.p to b.q;" + + "}"; + + ASTSysMLModel ast = SysMLv2Mill.parser().parse_String(validModel).get(); + SysMLv2Mill.scopesGenitorDelegator().createFromAST(ast); + var checker = new SysMLv2CoCoChecker(); + checker.addCoCo(new MKPX_CoCo4()); + checker.checkAll(ast); + assertTrue(Log.getFindings().isEmpty()); + } + + @Test + public void testInvalid() throws IOException { + String invalidModel = + "part def A { port p: int; }" + + "part def B { port q: ~int; }" + + "part def System {" + + "part a: A;" + + "part b: B;" + + "connect a.wrongName to b.q;" + + "}"; + + ASTSysMLModel ast = SysMLv2Mill.parser().parse_String(invalidModel).get(); + SysMLv2Mill.scopesGenitorDelegator().createFromAST(ast); + var checker = new SysMLv2CoCoChecker(); + checker.addCoCo(new MKPX_CoCo4()); + Log.enableFailQuick(false); + checker.checkAll(ast); + assertTrue(Log.getFindings().stream() + .anyMatch(f -> f.getMsg().contains("0xMKPX04"))); + } + } +}