diff options
author | Kristóf Marussy <marussy@mit.bme.hu> | 2023-11-19 14:58:55 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-19 14:58:55 +0100 |
commit | df0f7cbf05558ce8691759256caa64ab01dd5d9c (patch) | |
tree | 0a842960e27ad25d458c18f7bd9f8b4bf5b89b6c /subprojects | |
parent | build: prepare for Maven publication (diff) | |
parent | feat(langauge): validate exists and equals (diff) | |
download | refinery-df0f7cbf05558ce8691759256caa64ab01dd5d9c.tar.gz refinery-df0f7cbf05558ce8691759256caa64ab01dd5d9c.tar.zst refinery-df0f7cbf05558ce8691759256caa64ab01dd5d9c.zip |
Merge pull request #48 from kris7t/validator
Improve content assist and validator
Diffstat (limited to 'subprojects')
39 files changed, 2148 insertions, 147 deletions
diff --git a/subprojects/frontend/config/detectDevModeOptions.ts b/subprojects/frontend/config/detectDevModeOptions.ts index 6052e047..7a3de4cb 100644 --- a/subprojects/frontend/config/detectDevModeOptions.ts +++ b/subprojects/frontend/config/detectDevModeOptions.ts | |||
@@ -63,6 +63,11 @@ export default function detectDevModeOptions(): DevModeOptions { | |||
63 | const api = detectListenOptions('API', '127.0.0.1', 1312); | 63 | const api = detectListenOptions('API', '127.0.0.1', 1312); |
64 | const publicAddress = detectListenOptions('PUBLIC', listen.host, listen.port); | 64 | const publicAddress = detectListenOptions('PUBLIC', listen.host, listen.port); |
65 | 65 | ||
66 | if (listen.secure) { | ||
67 | // Since nodejs 20, we'd need to pass in HTTPS options manually. | ||
68 | throw new Error(`Preview on secure port ${listen.port} is not supported`); | ||
69 | } | ||
70 | |||
66 | const backendConfig: BackendConfig = { | 71 | const backendConfig: BackendConfig = { |
67 | webSocketURL: `${listenURL(publicAddress, 'ws')}/${API_ENDPOINT}`, | 72 | webSocketURL: `${listenURL(publicAddress, 'ws')}/${API_ENDPOINT}`, |
68 | }; | 73 | }; |
@@ -75,7 +80,6 @@ export default function detectDevModeOptions(): DevModeOptions { | |||
75 | host: listen.host, | 80 | host: listen.host, |
76 | port: listen.port, | 81 | port: listen.port, |
77 | strictPort: true, | 82 | strictPort: true, |
78 | https: listen.secure, | ||
79 | headers: { | 83 | headers: { |
80 | // Enable strict origin isolation, see e.g., | 84 | // Enable strict origin isolation, see e.g., |
81 | // https://github.com/vitejs/vite/issues/3909#issuecomment-1065893956 | 85 | // https://github.com/vitejs/vite/issues/3909#issuecomment-1065893956 |
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index 748cc78d..e710a506 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json | |||
@@ -28,74 +28,74 @@ | |||
28 | }, | 28 | }, |
29 | "homepage": "https://refinery.tools", | 29 | "homepage": "https://refinery.tools", |
30 | "dependencies": { | 30 | "dependencies": { |
31 | "@codemirror/autocomplete": "^6.10.2", | 31 | "@codemirror/autocomplete": "^6.11.0", |
32 | "@codemirror/commands": "^6.3.0", | 32 | "@codemirror/commands": "^6.3.0", |
33 | "@codemirror/language": "^6.9.1", | 33 | "@codemirror/language": "^6.9.2", |
34 | "@codemirror/lint": "^6.4.2", | 34 | "@codemirror/lint": "^6.4.2", |
35 | "@codemirror/search": "^6.5.4", | 35 | "@codemirror/search": "^6.5.4", |
36 | "@codemirror/state": "^6.3.1", | 36 | "@codemirror/state": "^6.3.1", |
37 | "@codemirror/view": "^6.21.3", | 37 | "@codemirror/view": "^6.22.0", |
38 | "@emotion/react": "^11.11.1", | 38 | "@emotion/react": "^11.11.1", |
39 | "@emotion/styled": "^11.11.0", | 39 | "@emotion/styled": "^11.11.0", |
40 | "@fontsource-variable/jetbrains-mono": "^5.0.16", | 40 | "@fontsource-variable/jetbrains-mono": "^5.0.18", |
41 | "@fontsource-variable/open-sans": "^5.0.16", | 41 | "@fontsource-variable/open-sans": "^5.0.18", |
42 | "@hpcc-js/wasm": "^2.14.1", | 42 | "@hpcc-js/wasm": "^2.14.1", |
43 | "@lezer/common": "^1.1.0", | 43 | "@lezer/common": "^1.1.1", |
44 | "@lezer/highlight": "^1.1.6", | 44 | "@lezer/highlight": "^1.2.0", |
45 | "@lezer/lr": "^1.3.13", | 45 | "@lezer/lr": "^1.3.14", |
46 | "@material-icons/svg": "^1.0.33", | 46 | "@material-icons/svg": "^1.0.33", |
47 | "@mui/icons-material": "5.14.14", | 47 | "@mui/icons-material": "5.14.18", |
48 | "@mui/material": "5.14.14", | 48 | "@mui/material": "5.14.18", |
49 | "@mui/system": "^5.14.14", | 49 | "@mui/system": "^5.14.18", |
50 | "@mui/x-data-grid": "^6.16.2", | 50 | "@mui/x-data-grid": "^6.18.1", |
51 | "ansi-styles": "^6.2.1", | 51 | "ansi-styles": "^6.2.1", |
52 | "csstype": "^3.1.2", | 52 | "csstype": "^3.1.2", |
53 | "d3": "^7.8.5", | 53 | "d3": "^7.8.5", |
54 | "d3-graphviz": "patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch", | 54 | "d3-graphviz": "patch:d3-graphviz@npm%3A5.2.0#~/.yarn/patches/d3-graphviz-npm-5.2.0-161b1fbad4.patch", |
55 | "d3-selection": "^3.0.0", | 55 | "d3-selection": "^3.0.0", |
56 | "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", | 56 | "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", |
57 | "escape-string-regexp": "^5.0.0", | 57 | "escape-string-regexp": "^5.0.0", |
58 | "lodash-es": "^4.17.21", | 58 | "lodash-es": "^4.17.21", |
59 | "loglevel": "^1.8.1", | 59 | "loglevel": "^1.8.1", |
60 | "loglevel-plugin-prefix": "^0.8.4", | 60 | "loglevel-plugin-prefix": "^0.8.4", |
61 | "mobx": "^6.10.2", | 61 | "mobx": "^6.11.0", |
62 | "mobx-react-lite": "^4.0.5", | 62 | "mobx-react-lite": "^4.0.5", |
63 | "ms": "^2.1.3", | 63 | "ms": "^2.1.3", |
64 | "nanoid": "^5.0.2", | 64 | "nanoid": "^5.0.3", |
65 | "notistack": "^3.0.1", | 65 | "notistack": "^3.0.1", |
66 | "react": "^18.2.0", | 66 | "react": "^18.2.0", |
67 | "react-dom": "^18.2.0", | 67 | "react-dom": "^18.2.0", |
68 | "react-resize-detector": "^9.1.0", | 68 | "react-resize-detector": "^9.1.0", |
69 | "xstate": "^4.38.2", | 69 | "xstate": "^4.38.3", |
70 | "zod": "^3.22.4" | 70 | "zod": "^3.22.4" |
71 | }, | 71 | }, |
72 | "devDependencies": { | 72 | "devDependencies": { |
73 | "@lezer/generator": "^1.5.1", | 73 | "@lezer/generator": "^1.5.1", |
74 | "@types/d3": "^7.4.2", | 74 | "@types/d3": "^7.4.3", |
75 | "@types/d3-graphviz": "^2.6.9", | 75 | "@types/d3-graphviz": "^2.6.10", |
76 | "@types/d3-selection": "^3.0.8", | 76 | "@types/d3-selection": "^3.0.10", |
77 | "@types/d3-zoom": "^3.0.6", | 77 | "@types/d3-zoom": "^3.0.8", |
78 | "@types/eslint": "^8.44.6", | 78 | "@types/eslint": "^8.44.7", |
79 | "@types/html-minifier-terser": "^7.0.1", | 79 | "@types/html-minifier-terser": "^7.0.2", |
80 | "@types/lodash-es": "^4.17.10", | 80 | "@types/lodash-es": "^4.17.11", |
81 | "@types/micromatch": "^4.0.4", | 81 | "@types/micromatch": "^4.0.5", |
82 | "@types/ms": "^0.7.33", | 82 | "@types/ms": "^0.7.34", |
83 | "@types/node": "^18.18.6", | 83 | "@types/node": "^20.9.2", |
84 | "@types/pnpapi": "^0.0.4", | 84 | "@types/pnpapi": "^0.0.5", |
85 | "@types/react": "^18.2.29", | 85 | "@types/react": "^18.2.37", |
86 | "@types/react-dom": "^18.2.14", | 86 | "@types/react-dom": "^18.2.15", |
87 | "@typescript-eslint/eslint-plugin": "^6.8.0", | 87 | "@typescript-eslint/eslint-plugin": "^6.11.0", |
88 | "@typescript-eslint/parser": "^6.8.0", | 88 | "@typescript-eslint/parser": "^6.11.0", |
89 | "@vitejs/plugin-react-swc": "^3.4.0", | 89 | "@vitejs/plugin-react-swc": "^3.5.0", |
90 | "@xstate/cli": "^0.5.7", | 90 | "@xstate/cli": "^0.5.11", |
91 | "cross-env": "^7.0.3", | 91 | "cross-env": "^7.0.3", |
92 | "eslint": "^8.51.0", | 92 | "eslint": "^8.54.0", |
93 | "eslint-config-airbnb": "^19.0.4", | 93 | "eslint-config-airbnb": "^19.0.4", |
94 | "eslint-config-airbnb-typescript": "^17.1.0", | 94 | "eslint-config-airbnb-typescript": "^17.1.0", |
95 | "eslint-config-prettier": "^9.0.0", | 95 | "eslint-config-prettier": "^9.0.0", |
96 | "eslint-import-resolver-typescript": "^3.6.1", | 96 | "eslint-import-resolver-typescript": "^3.6.1", |
97 | "eslint-plugin-import": "^2.28.1", | 97 | "eslint-plugin-import": "^2.29.0", |
98 | "eslint-plugin-jsx-a11y": "^6.7.1", | 98 | "eslint-plugin-jsx-a11y": "^6.8.0", |
99 | "eslint-plugin-mobx": "^0.0.9", | 99 | "eslint-plugin-mobx": "^0.0.9", |
100 | "eslint-plugin-prettier": "^5.0.1", | 100 | "eslint-plugin-prettier": "^5.0.1", |
101 | "eslint-plugin-react": "^7.33.2", | 101 | "eslint-plugin-react": "^7.33.2", |
@@ -103,10 +103,10 @@ | |||
103 | "html-minifier-terser": "^7.2.0", | 103 | "html-minifier-terser": "^7.2.0", |
104 | "micromatch": "^4.0.5", | 104 | "micromatch": "^4.0.5", |
105 | "pnpapi": "^0.0.0", | 105 | "pnpapi": "^0.0.0", |
106 | "prettier": "^3.0.3", | 106 | "prettier": "^3.1.0", |
107 | "typescript": "5.2.2", | 107 | "typescript": "5.2.2", |
108 | "vite": "^4.5.0", | 108 | "vite": "^5.0.0", |
109 | "vite-plugin-pwa": "^0.16.5", | 109 | "vite-plugin-pwa": "^0.17.0", |
110 | "workbox-window": "^7.0.0" | 110 | "workbox-window": "^7.0.0" |
111 | } | 111 | } |
112 | } | 112 | } |
diff --git a/subprojects/frontend/src/DirectionalSplitPane.tsx b/subprojects/frontend/src/DirectionalSplitPane.tsx index 59c8b739..110bb202 100644 --- a/subprojects/frontend/src/DirectionalSplitPane.tsx +++ b/subprojects/frontend/src/DirectionalSplitPane.tsx | |||
@@ -64,9 +64,8 @@ export default function DirectionalSplitPane({ | |||
64 | [axis]: '0px', | 64 | [axis]: '0px', |
65 | display: showLeftOnly || showRightOnly ? 'none' : 'flex', | 65 | display: showLeftOnly || showRightOnly ? 'none' : 'flex', |
66 | flexDirection: direction, | 66 | flexDirection: direction, |
67 | [horizontalSplit | 67 | [horizontalSplit ? 'borderBottom' : 'borderRight']: |
68 | ? 'borderBottom' | 68 | `1px solid ${theme.palette.outer.border}`, |
69 | : 'borderRight']: `1px solid ${theme.palette.outer.border}`, | ||
70 | }} | 69 | }} |
71 | > | 70 | > |
72 | <Box | 71 | <Box |
diff --git a/subprojects/frontend/src/graph/VisibilityDialog.tsx b/subprojects/frontend/src/graph/VisibilityDialog.tsx index f1fef28b..bfdcd59f 100644 --- a/subprojects/frontend/src/graph/VisibilityDialog.tsx +++ b/subprojects/frontend/src/graph/VisibilityDialog.tsx | |||
@@ -210,7 +210,10 @@ function VisibilityDialog({ | |||
210 | } | 210 | } |
211 | /> | 211 | /> |
212 | </td> | 212 | </td> |
213 | <td onClick={() => graph.cycleVisibility(name)}> | 213 | <td |
214 | onClick={() => graph.cycleVisibility(name)} | ||
215 | aria-label="Toggle visiblity" | ||
216 | > | ||
214 | <div className="VisibilityDialog-nowrap"> | 217 | <div className="VisibilityDialog-nowrap"> |
215 | <RelationName metadata={metadata} abbreviate={graph.abbreviate} /> | 218 | <RelationName metadata={metadata} abbreviate={graph.abbreviate} /> |
216 | </div> | 219 | </div> |
@@ -261,10 +264,10 @@ function VisibilityDialog({ | |||
261 | <table cellSpacing={0}> | 264 | <table cellSpacing={0}> |
262 | <thead> | 265 | <thead> |
263 | <tr> | 266 | <tr> |
264 | <th> | 267 | <th aria-label="Show true and error values"> |
265 | <LabelIcon /> | 268 | <LabelIcon /> |
266 | </th> | 269 | </th> |
267 | <th> | 270 | <th aria-label="Show unknown values"> |
268 | <LabelOutlinedIcon /> | 271 | <LabelOutlinedIcon /> |
269 | </th> | 272 | </th> |
270 | <th>Symbol</th> | 273 | <th>Symbol</th> |
diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java index ea90a82e..166b4400 100644 --- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java +++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java | |||
@@ -6,9 +6,12 @@ | |||
6 | package tools.refinery.language.ide.contentassist; | 6 | package tools.refinery.language.ide.contentassist; |
7 | 7 | ||
8 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
9 | import org.eclipse.emf.ecore.EClass; | ||
9 | import org.eclipse.emf.ecore.EObject; | 10 | import org.eclipse.emf.ecore.EObject; |
11 | import org.eclipse.emf.ecore.EReference; | ||
10 | import org.eclipse.emf.ecore.util.EcoreUtil; | 12 | import org.eclipse.emf.ecore.util.EcoreUtil; |
11 | import org.eclipse.xtext.CrossReference; | 13 | import org.eclipse.xtext.CrossReference; |
14 | import org.eclipse.xtext.EcoreUtil2; | ||
12 | import org.eclipse.xtext.GrammarUtil; | 15 | import org.eclipse.xtext.GrammarUtil; |
13 | import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext; | 16 | import org.eclipse.xtext.ide.editor.contentassist.ContentAssistContext; |
14 | import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider; | 17 | import org.eclipse.xtext.ide.editor.contentassist.IdeCrossrefProposalProvider; |
@@ -16,10 +19,14 @@ import org.eclipse.xtext.naming.QualifiedName; | |||
16 | import org.eclipse.xtext.nodemodel.util.NodeModelUtils; | 19 | import org.eclipse.xtext.nodemodel.util.NodeModelUtils; |
17 | import org.eclipse.xtext.resource.IEObjectDescription; | 20 | import org.eclipse.xtext.resource.IEObjectDescription; |
18 | import org.eclipse.xtext.scoping.IScope; | 21 | import org.eclipse.xtext.scoping.IScope; |
19 | import tools.refinery.language.model.problem.Problem; | 22 | import org.eclipse.xtext.xtext.CurrentTypeFinder; |
23 | import org.jetbrains.annotations.Nullable; | ||
24 | import tools.refinery.language.model.problem.*; | ||
20 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | 25 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; |
21 | import tools.refinery.language.resource.ReferenceCounter; | 26 | import tools.refinery.language.utils.BuiltinSymbols; |
27 | import tools.refinery.language.utils.ProblemDesugarer; | ||
22 | import tools.refinery.language.utils.ProblemUtil; | 28 | import tools.refinery.language.utils.ProblemUtil; |
29 | import tools.refinery.language.validation.ReferenceCounter; | ||
23 | 30 | ||
24 | import java.util.ArrayList; | 31 | import java.util.ArrayList; |
25 | import java.util.HashMap; | 32 | import java.util.HashMap; |
@@ -28,8 +35,14 @@ import java.util.Objects; | |||
28 | 35 | ||
29 | public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider { | 36 | public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider { |
30 | @Inject | 37 | @Inject |
38 | private CurrentTypeFinder currentTypeFinder; | ||
39 | |||
40 | @Inject | ||
31 | private ReferenceCounter referenceCounter; | 41 | private ReferenceCounter referenceCounter; |
32 | 42 | ||
43 | @Inject | ||
44 | private ProblemDesugarer desugarer; | ||
45 | |||
33 | @Override | 46 | @Override |
34 | protected Iterable<IEObjectDescription> queryScope(IScope scope, CrossReference crossReference, | 47 | protected Iterable<IEObjectDescription> queryScope(IScope scope, CrossReference crossReference, |
35 | ContentAssistContext context) { | 48 | ContentAssistContext context) { |
@@ -49,7 +62,7 @@ public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider | |||
49 | for (var candidates : eObjectDescriptionsByName.values()) { | 62 | for (var candidates : eObjectDescriptionsByName.values()) { |
50 | if (candidates.size() == 1) { | 63 | if (candidates.size() == 1) { |
51 | var candidate = candidates.get(0); | 64 | var candidate = candidates.get(0); |
52 | if (shouldBeVisible(candidate)) { | 65 | if (shouldBeVisible(candidate, crossReference, context)) { |
53 | eObjectDescriptions.add(candidate); | 66 | eObjectDescriptions.add(candidate); |
54 | } | 67 | } |
55 | } | 68 | } |
@@ -81,9 +94,106 @@ public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider | |||
81 | return true; | 94 | return true; |
82 | } | 95 | } |
83 | 96 | ||
84 | protected boolean shouldBeVisible(IEObjectDescription candidate) { | 97 | protected boolean shouldBeVisible(IEObjectDescription candidate, CrossReference crossReference, |
98 | ContentAssistContext context) { | ||
85 | var errorPredicate = candidate.getUserData(ProblemResourceDescriptionStrategy.ERROR_PREDICATE); | 99 | var errorPredicate = candidate.getUserData(ProblemResourceDescriptionStrategy.ERROR_PREDICATE); |
86 | return !ProblemResourceDescriptionStrategy.ERROR_PREDICATE_TRUE.equals(errorPredicate); | 100 | if (ProblemResourceDescriptionStrategy.ERROR_PREDICATE_TRUE.equals(errorPredicate)) { |
101 | return false; | ||
102 | } | ||
103 | |||
104 | var eReference = getEReference(crossReference); | ||
105 | if (eReference == null) { | ||
106 | return true; | ||
107 | } | ||
108 | |||
109 | var candidateEObjectOrProxy = candidate.getEObjectOrProxy(); | ||
110 | |||
111 | if (eReference.equals(ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) && | ||
112 | candidateEObjectOrProxy instanceof ReferenceDeclaration candidateReferenceDeclaration) { | ||
113 | return oppositeShouldBeVisible(candidateReferenceDeclaration, context); | ||
114 | } | ||
115 | |||
116 | var builtinSymbolsOption = desugarer.getBuiltinSymbols(context.getRootModel()); | ||
117 | if (builtinSymbolsOption.isEmpty()) { | ||
118 | return true; | ||
119 | } | ||
120 | var builtinSymbols = builtinSymbolsOption.get(); | ||
121 | |||
122 | return builtinSymbolAwareShouldBeVisible(candidate, context, eReference, builtinSymbols, | ||
123 | candidateEObjectOrProxy); | ||
124 | } | ||
125 | |||
126 | private static boolean oppositeShouldBeVisible(ReferenceDeclaration candidateReferenceDeclaration, | ||
127 | ContentAssistContext context) { | ||
128 | var referenceDeclaration = EcoreUtil2.getContainerOfType(context.getCurrentModel(), | ||
129 | ReferenceDeclaration.class); | ||
130 | if (referenceDeclaration == null) { | ||
131 | return true; | ||
132 | } | ||
133 | var classDeclaration = EcoreUtil2.getContainerOfType(referenceDeclaration, ClassDeclaration.class); | ||
134 | if (classDeclaration == null) { | ||
135 | return true; | ||
136 | } | ||
137 | var oppositeType = candidateReferenceDeclaration.getReferenceType(); | ||
138 | if (oppositeType == null) { | ||
139 | return true; | ||
140 | } | ||
141 | var resolvedOppositeType = EcoreUtil.resolve(oppositeType, candidateReferenceDeclaration); | ||
142 | return classDeclaration.equals(resolvedOppositeType); | ||
143 | } | ||
144 | |||
145 | private boolean builtinSymbolAwareShouldBeVisible( | ||
146 | IEObjectDescription candidate, ContentAssistContext context, EReference eReference, | ||
147 | BuiltinSymbols builtinSymbols, EObject candidateEObjectOrProxy) { | ||
148 | if (eReference.equals(ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE) && | ||
149 | context.getCurrentModel() instanceof ReferenceDeclaration referenceDeclaration && | ||
150 | (referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT || | ||
151 | referenceDeclaration.getKind() == ReferenceKind.CONTAINER)) { | ||
152 | // Containment or container references must have a class type. | ||
153 | // We don't support {@code node} as a container or contained type. | ||
154 | return ProblemPackage.Literals.CLASS_DECLARATION.isSuperTypeOf(candidate.getEClass()) && | ||
155 | !builtinSymbols.node().equals(candidateEObjectOrProxy); | ||
156 | } | ||
157 | |||
158 | if (eReference.equals(ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE) || | ||
159 | eReference.equals(ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE) || | ||
160 | eReference.equals(ProblemPackage.Literals.TYPE_SCOPE__TARGET_TYPE)) { | ||
161 | if (builtinSymbols.exists().equals(candidateEObjectOrProxy)) { | ||
162 | return false; | ||
163 | } | ||
164 | var arity = candidate.getUserData(ProblemResourceDescriptionStrategy.ARITY); | ||
165 | return arity == null || arity.equals("1"); | ||
166 | } | ||
167 | |||
168 | if (eReference.equals(ProblemPackage.Literals.CLASS_DECLARATION__SUPER_TYPES)) { | ||
169 | return supertypeShouldBeVisible(candidate, context, builtinSymbols, candidateEObjectOrProxy); | ||
170 | } | ||
171 | |||
172 | return true; | ||
173 | } | ||
174 | |||
175 | private boolean supertypeShouldBeVisible(IEObjectDescription candidate, ContentAssistContext context, | ||
176 | BuiltinSymbols builtinSymbols, EObject candidateEObjectOrProxy) { | ||
177 | if (!ProblemPackage.Literals.CLASS_DECLARATION.isSuperTypeOf(candidate.getEClass()) || | ||
178 | builtinSymbols.node().equals(candidateEObjectOrProxy) || | ||
179 | builtinSymbols.contained().equals(candidateEObjectOrProxy)) { | ||
180 | return false; | ||
181 | } | ||
182 | if (context.getCurrentModel() instanceof ClassDeclaration classDeclaration && | ||
183 | candidateEObjectOrProxy instanceof ClassDeclaration candidateClassDeclaration) { | ||
184 | return !classDeclaration.equals(candidateClassDeclaration) && | ||
185 | !classDeclaration.getSuperTypes().contains(candidateClassDeclaration); | ||
186 | } | ||
187 | return true; | ||
188 | } | ||
189 | |||
190 | @Nullable | ||
191 | private EReference getEReference(CrossReference crossReference) { | ||
192 | var type = currentTypeFinder.findCurrentTypeAfter(crossReference); | ||
193 | if (!(type instanceof EClass eClass)) { | ||
194 | return null; | ||
195 | } | ||
196 | return GrammarUtil.getReference(crossReference, eClass); | ||
87 | } | 197 | } |
88 | 198 | ||
89 | protected EObject getCurrentValue(CrossReference crossRef, ContentAssistContext context) { | 199 | protected EObject getCurrentValue(CrossReference crossRef, ContentAssistContext context) { |
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java index b3c58366..ecaa7e0d 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/ModelInitializer.java | |||
@@ -63,6 +63,8 @@ public class ModelInitializer { | |||
63 | 63 | ||
64 | private final Map<PartialRelation, RelationInfo> partialRelationInfoMap = new HashMap<>(); | 64 | private final Map<PartialRelation, RelationInfo> partialRelationInfoMap = new HashMap<>(); |
65 | 65 | ||
66 | private final Set<PartialRelation> targetTypes = new HashSet<>(); | ||
67 | |||
66 | private final MetamodelBuilder metamodelBuilder = Metamodel.builder(); | 68 | private final MetamodelBuilder metamodelBuilder = Metamodel.builder(); |
67 | 69 | ||
68 | private Metamodel metamodel; | 70 | private Metamodel metamodel; |
@@ -285,6 +287,7 @@ public class ModelInitializer { | |||
285 | var relation = getPartialRelation(referenceDeclaration); | 287 | var relation = getPartialRelation(referenceDeclaration); |
286 | var source = getPartialRelation(classDeclaration); | 288 | var source = getPartialRelation(classDeclaration); |
287 | var target = getPartialRelation(referenceDeclaration.getReferenceType()); | 289 | var target = getPartialRelation(referenceDeclaration.getReferenceType()); |
290 | targetTypes.add(target); | ||
288 | boolean containment = referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT; | 291 | boolean containment = referenceDeclaration.getKind() == ReferenceKind.CONTAINMENT; |
289 | var opposite = referenceDeclaration.getOpposite(); | 292 | var opposite = referenceDeclaration.getOpposite(); |
290 | PartialRelation oppositeRelation = null; | 293 | PartialRelation oppositeRelation = null; |
@@ -473,17 +476,16 @@ public class ModelInitializer { | |||
473 | private void collectPredicateDefinition(PredicateDefinition predicateDefinition, ModelStoreBuilder storeBuilder) { | 476 | private void collectPredicateDefinition(PredicateDefinition predicateDefinition, ModelStoreBuilder storeBuilder) { |
474 | var partialRelation = getPartialRelation(predicateDefinition); | 477 | var partialRelation = getPartialRelation(predicateDefinition); |
475 | var query = toQuery(partialRelation.name(), predicateDefinition); | 478 | var query = toQuery(partialRelation.name(), predicateDefinition); |
476 | boolean mutable; | 479 | boolean mutable = targetTypes.contains(partialRelation); |
477 | TruthValue defaultValue; | 480 | TruthValue defaultValue; |
478 | if (predicateDefinition.isError()) { | 481 | if (predicateDefinition.isError()) { |
479 | mutable = false; | ||
480 | defaultValue = TruthValue.FALSE; | 482 | defaultValue = TruthValue.FALSE; |
481 | } else { | 483 | } else { |
482 | var seed = modelSeed.getSeed(partialRelation); | 484 | var seed = modelSeed.getSeed(partialRelation); |
483 | defaultValue = seed.reducedValue() == TruthValue.FALSE ? TruthValue.FALSE : TruthValue.UNKNOWN; | 485 | defaultValue = seed.reducedValue() == TruthValue.FALSE ? TruthValue.FALSE : TruthValue.UNKNOWN; |
484 | var cursor = seed.getCursor(defaultValue, problemTrace.getNodeTrace().size()); | 486 | var cursor = seed.getCursor(defaultValue, problemTrace.getNodeTrace().size()); |
485 | // The symbol should be mutable if there is at least one non-default entry in the seed. | 487 | // The symbol should be mutable if there is at least one non-default entry in the seed. |
486 | mutable = cursor.move(); | 488 | mutable = mutable || cursor.move(); |
487 | } | 489 | } |
488 | var translator = new PredicateTranslator(partialRelation, query, mutable, defaultValue); | 490 | var translator = new PredicateTranslator(partialRelation, query, mutable, defaultValue); |
489 | storeBuilder.with(translator); | 491 | storeBuilder.with(translator); |
diff --git a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java index 899e3cb3..b4abce81 100644 --- a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java +++ b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java | |||
@@ -77,7 +77,7 @@ class ModelGenerationTest { | |||
77 | % Scope | 77 | % Scope |
78 | scope Post = 5, Person = 5. | 78 | scope Post = 5, Person = 5. |
79 | """); | 79 | """); |
80 | assertThat(parsedProblem.errors(), empty()); | 80 | assertThat(parsedProblem.getResourceErrors(), empty()); |
81 | var problem = parsedProblem.problem(); | 81 | var problem = parsedProblem.problem(); |
82 | 82 | ||
83 | var storeBuilder = ModelStore.builder() | 83 | var storeBuilder = ModelStore.builder() |
@@ -211,7 +211,7 @@ class ModelGenerationTest { | |||
211 | 211 | ||
212 | scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1. | 212 | scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1. |
213 | """); | 213 | """); |
214 | assertThat(parsedProblem.errors(), empty()); | 214 | assertThat(parsedProblem.getResourceErrors(), empty()); |
215 | var problem = parsedProblem.problem(); | 215 | var problem = parsedProblem.problem(); |
216 | 216 | ||
217 | var storeBuilder = ModelStore.builder() | 217 | var storeBuilder = ModelStore.builder() |
@@ -278,7 +278,7 @@ class ModelGenerationTest { | |||
278 | 278 | ||
279 | scope Filesystem += 0, Entry = 100. | 279 | scope Filesystem += 0, Entry = 100. |
280 | """); | 280 | """); |
281 | assertThat(parsedProblem.errors(), empty()); | 281 | assertThat(parsedProblem.getResourceErrors(), empty()); |
282 | var problem = parsedProblem.problem(); | 282 | var problem = parsedProblem.problem(); |
283 | 283 | ||
284 | var storeBuilder = ModelStore.builder() | 284 | var storeBuilder = ModelStore.builder() |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext index 0a91178b..0fb96954 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext +++ b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext | |||
@@ -40,10 +40,13 @@ enum ReferenceKind: | |||
40 | ReferenceDeclaration: | 40 | ReferenceDeclaration: |
41 | (referenceType=[Relation|NonContainmentQualifiedName] | | 41 | (referenceType=[Relation|NonContainmentQualifiedName] | |
42 | kind=ReferenceKind referenceType=[Relation|QualifiedName]) | 42 | kind=ReferenceKind referenceType=[Relation|QualifiedName]) |
43 | ("[" multiplicity=Multiplicity "]")? | 43 | (multiplicity=ReferenceMultiplicity)? |
44 | name=Identifier | 44 | name=Identifier |
45 | ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?; | 45 | ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?; |
46 | 46 | ||
47 | ReferenceMultiplicity returns Multiplicity: | ||
48 | "[" Multiplicity "]"; | ||
49 | |||
47 | //enum PrimitiveType: | 50 | //enum PrimitiveType: |
48 | // INT="int" | REAL="real" | STRING="string"; | 51 | // INT="int" | REAL="real" | STRING="string"; |
49 | // | 52 | // |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java index 2636a8ee..0a5cb3c2 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java +++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java | |||
@@ -12,6 +12,7 @@ package tools.refinery.language; | |||
12 | import com.google.inject.Binder; | 12 | import com.google.inject.Binder; |
13 | import com.google.inject.name.Names; | 13 | import com.google.inject.name.Names; |
14 | import org.eclipse.xtext.conversion.IValueConverterService; | 14 | import org.eclipse.xtext.conversion.IValueConverterService; |
15 | import org.eclipse.xtext.linking.ILinkingService; | ||
15 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | 16 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
16 | import org.eclipse.xtext.parser.IParser; | 17 | import org.eclipse.xtext.parser.IParser; |
17 | import org.eclipse.xtext.resource.*; | 18 | import org.eclipse.xtext.resource.*; |
@@ -19,17 +20,21 @@ import org.eclipse.xtext.scoping.IGlobalScopeProvider; | |||
19 | import org.eclipse.xtext.scoping.IScopeProvider; | 20 | import org.eclipse.xtext.scoping.IScopeProvider; |
20 | import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; | 21 | import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; |
21 | import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; | 22 | import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; |
23 | import org.eclipse.xtext.validation.IDiagnosticConverter; | ||
22 | import org.eclipse.xtext.validation.IResourceValidator; | 24 | import org.eclipse.xtext.validation.IResourceValidator; |
23 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; | 25 | import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; |
24 | import tools.refinery.language.conversion.ProblemValueConverterService; | 26 | import tools.refinery.language.conversion.ProblemValueConverterService; |
27 | import tools.refinery.language.linking.ProblemLinkingService; | ||
25 | import tools.refinery.language.naming.ProblemQualifiedNameConverter; | 28 | import tools.refinery.language.naming.ProblemQualifiedNameConverter; |
26 | import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; | 29 | import tools.refinery.language.parser.antlr.TokenSourceInjectingProblemParser; |
27 | import tools.refinery.language.resource.ProblemDerivedStateComputer; | 30 | import tools.refinery.language.resource.ProblemDerivedStateComputer; |
28 | import tools.refinery.language.resource.ProblemLocationInFileProvider; | 31 | import tools.refinery.language.resource.ProblemLocationInFileProvider; |
32 | import tools.refinery.language.resource.ProblemResource; | ||
29 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | 33 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; |
30 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; | 34 | import tools.refinery.language.scoping.ProblemGlobalScopeProvider; |
31 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; | 35 | import tools.refinery.language.scoping.ProblemLocalScopeProvider; |
32 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; | 36 | import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; |
37 | import tools.refinery.language.validation.ProblemDiagnosticConverter; | ||
33 | 38 | ||
34 | /** | 39 | /** |
35 | * Use this class to register components to be used at runtime / without the | 40 | * Use this class to register components to be used at runtime / without the |
@@ -55,6 +60,11 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
55 | } | 60 | } |
56 | 61 | ||
57 | @Override | 62 | @Override |
63 | public Class<? extends ILinkingService> bindILinkingService() { | ||
64 | return ProblemLinkingService.class; | ||
65 | } | ||
66 | |||
67 | @Override | ||
58 | public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() { | 68 | public Class<? extends IGlobalScopeProvider> bindIGlobalScopeProvider() { |
59 | return ProblemGlobalScopeProvider.class; | 69 | return ProblemGlobalScopeProvider.class; |
60 | } | 70 | } |
@@ -67,7 +77,7 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
67 | 77 | ||
68 | @Override | 78 | @Override |
69 | public Class<? extends XtextResource> bindXtextResource() { | 79 | public Class<? extends XtextResource> bindXtextResource() { |
70 | return DerivedStateAwareResource.class; | 80 | return ProblemResource.class; |
71 | } | 81 | } |
72 | 82 | ||
73 | // Method name follows Xtext convention. | 83 | // Method name follows Xtext convention. |
@@ -93,4 +103,8 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
93 | public Class<? extends ISemanticSequencer> bindISemanticSequencer() { | 103 | public Class<? extends ISemanticSequencer> bindISemanticSequencer() { |
94 | return PreferShortAssertionsProblemSemanticSequencer.class; | 104 | return PreferShortAssertionsProblemSemanticSequencer.class; |
95 | } | 105 | } |
106 | |||
107 | public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() { | ||
108 | return ProblemDiagnosticConverter.class; | ||
109 | } | ||
96 | } | 110 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java b/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java new file mode 100644 index 00000000..511ed420 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/linking/ProblemLinkingService.java | |||
@@ -0,0 +1,72 @@ | |||
1 | /******************************************************************************* | ||
2 | * Copyright (c) 2008, 2018 itemis AG (http://www.itemis.eu) and others. | ||
3 | * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> | ||
4 | * This program and the accompanying materials are made available under the | ||
5 | * terms of the Eclipse Public License 2.0 which is available at | ||
6 | * http://www.eclipse.org/legal/epl-2.0. | ||
7 | * SPDX-License-Identifier: EPL-2.0 | ||
8 | *******************************************************************************/ | ||
9 | package tools.refinery.language.linking; | ||
10 | |||
11 | import com.google.inject.Inject; | ||
12 | import org.apache.log4j.Logger; | ||
13 | import org.eclipse.emf.ecore.EClass; | ||
14 | import org.eclipse.emf.ecore.EObject; | ||
15 | import org.eclipse.emf.ecore.EReference; | ||
16 | import org.eclipse.xtext.linking.impl.DefaultLinkingService; | ||
17 | import org.eclipse.xtext.linking.impl.IllegalNodeException; | ||
18 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
19 | import org.eclipse.xtext.naming.QualifiedName; | ||
20 | import org.eclipse.xtext.nodemodel.INode; | ||
21 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
22 | import org.eclipse.xtext.scoping.IScope; | ||
23 | |||
24 | import java.util.*; | ||
25 | |||
26 | public class ProblemLinkingService extends DefaultLinkingService { | ||
27 | @Inject | ||
28 | private IQualifiedNameConverter qualifiedNameConverter; | ||
29 | |||
30 | private static final Logger logger = Logger.getLogger(ProblemLinkingService.class); | ||
31 | |||
32 | @Override | ||
33 | public List<EObject> getLinkedObjects(EObject context, EReference ref, INode node) throws IllegalNodeException { | ||
34 | final EClass requiredType = ref.getEReferenceType(); | ||
35 | if (requiredType == null) { | ||
36 | return List.of(); | ||
37 | } | ||
38 | final String crossRefString = getCrossRefNodeAsString(node); | ||
39 | if (crossRefString == null || crossRefString.isEmpty()) { | ||
40 | return List.of(); | ||
41 | } | ||
42 | if (logger.isDebugEnabled()) { | ||
43 | logger.debug("before getLinkedObjects: node: '%s'".formatted(crossRefString)); | ||
44 | } | ||
45 | final IScope scope = getScope(context, ref); | ||
46 | if (scope == null) { | ||
47 | throw new AssertionError(("Scope provider must not return null for context %s, reference %s! Consider to" + | ||
48 | " return IScope.NULLSCOPE instead.").formatted(context, ref)); | ||
49 | } | ||
50 | final QualifiedName qualifiedLinkName = qualifiedNameConverter.toQualifiedName(crossRefString); | ||
51 | final Iterator<IEObjectDescription> iterator = scope.getElements(qualifiedLinkName).iterator(); | ||
52 | StringBuilder debug = null; | ||
53 | final Set<EObject> result = new LinkedHashSet<>(); | ||
54 | if (logger.isDebugEnabled()) { | ||
55 | debug = new StringBuilder() | ||
56 | .append("after getLinkedObjects: node: '") | ||
57 | .append(crossRefString) | ||
58 | .append("' result: "); | ||
59 | } | ||
60 | while (iterator.hasNext()) { | ||
61 | var eObjectDescription = iterator.next(); | ||
62 | if (debug != null) { | ||
63 | debug.append(eObjectDescription).append(", "); | ||
64 | } | ||
65 | result.add(eObjectDescription.getEObjectOrProxy()); | ||
66 | } | ||
67 | if (debug != null) { | ||
68 | logger.debug(debug); | ||
69 | } | ||
70 | return List.copyOf(result); | ||
71 | } | ||
72 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java new file mode 100644 index 00000000..43239ffe --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResource.java | |||
@@ -0,0 +1,263 @@ | |||
1 | /******************************************************************************* | ||
2 | * Copyright (c) 2008, 2023 itemis AG (http://www.itemis.eu) and others. | ||
3 | * Copyright (c) 2023 The Refinery Authors <https://refinery.tools/> | ||
4 | * This program and the accompanying materials are made available under the | ||
5 | * terms of the Eclipse Public License 2.0 which is available at | ||
6 | * http://www.eclipse.org/legal/epl-2.0. | ||
7 | * SPDX-License-Identifier: EPL-2.0 | ||
8 | *******************************************************************************/ | ||
9 | package tools.refinery.language.resource; | ||
10 | |||
11 | import com.google.inject.Inject; | ||
12 | import org.apache.log4j.Logger; | ||
13 | import org.eclipse.emf.ecore.EObject; | ||
14 | import org.eclipse.emf.ecore.EReference; | ||
15 | import org.eclipse.xtext.EcoreUtil2; | ||
16 | import org.eclipse.xtext.diagnostics.DiagnosticMessage; | ||
17 | import org.eclipse.xtext.diagnostics.Severity; | ||
18 | import org.eclipse.xtext.linking.ILinkingDiagnosticMessageProvider; | ||
19 | import org.eclipse.xtext.linking.impl.IllegalNodeException; | ||
20 | import org.eclipse.xtext.linking.impl.XtextLinkingDiagnostic; | ||
21 | import org.eclipse.xtext.linking.lazy.LazyLinkingResource; | ||
22 | import org.eclipse.xtext.nodemodel.INode; | ||
23 | import org.eclipse.xtext.resource.DerivedStateAwareResource; | ||
24 | import org.eclipse.xtext.util.Triple; | ||
25 | import org.jetbrains.annotations.Nullable; | ||
26 | |||
27 | import java.util.Arrays; | ||
28 | import java.util.List; | ||
29 | import java.util.Objects; | ||
30 | import java.util.Set; | ||
31 | |||
32 | public class ProblemResource extends DerivedStateAwareResource { | ||
33 | private static final Logger log = Logger.getLogger(ProblemResource.class); | ||
34 | |||
35 | @Inject | ||
36 | private ILinkingDiagnosticMessageProvider.Extended linkingDiagnosticMessageProvider; | ||
37 | |||
38 | /** | ||
39 | * Our own version of this field, because the original is not accessible. | ||
40 | */ | ||
41 | private int cyclicLinkingDetectionCounter = 0; | ||
42 | |||
43 | /** | ||
44 | * Tries to resolve a reference and emits a diagnostic if the reference is unresolvable or ambiguous. | ||
45 | * <p> | ||
46 | * This method was copied from {@link LazyLinkingResource#getEObject(String, Triple)}, but we modified it to also | ||
47 | * handle ambiguous references. | ||
48 | * | ||
49 | * @param uriFragment The URI fragment to resolve. | ||
50 | * @param triple The linking triple. | ||
51 | * @return The resolved {@link EObject}. | ||
52 | * @throws AssertionError If the URI fragment is unresolvable. | ||
53 | */ | ||
54 | @Override | ||
55 | protected EObject getEObject(String uriFragment, Triple<EObject, EReference, INode> triple) throws AssertionError { | ||
56 | cyclicLinkingDetectionCounter++; | ||
57 | if (cyclicLinkingDetectionCounter > cyclicLinkingDectectionCounterLimit && !resolving.add(triple)) { | ||
58 | return handleCyclicResolution(triple); | ||
59 | } | ||
60 | try { | ||
61 | Set<String> unresolvableProxies = getUnresolvableURIFragments(); | ||
62 | if (unresolvableProxies.contains(uriFragment)) { | ||
63 | return null; | ||
64 | } | ||
65 | var result = doGetEObject(triple); | ||
66 | if (result == null) { | ||
67 | if (isUnresolveableProxyCacheable(triple)) { | ||
68 | unresolvableProxies.add(uriFragment); | ||
69 | } | ||
70 | } else { | ||
71 | // remove previously added error markers, since everything should be fine now | ||
72 | unresolvableProxies.remove(uriFragment); | ||
73 | } | ||
74 | return result; | ||
75 | } catch (IllegalNodeException e) { | ||
76 | createAndAddDiagnostic(triple, e); | ||
77 | return null; | ||
78 | } finally { | ||
79 | if (cyclicLinkingDetectionCounter > cyclicLinkingDectectionCounterLimit) { | ||
80 | resolving.remove(triple); | ||
81 | } | ||
82 | cyclicLinkingDetectionCounter--; | ||
83 | } | ||
84 | } | ||
85 | |||
86 | @Nullable | ||
87 | private EObject doGetEObject(Triple<EObject, EReference, INode> triple) { | ||
88 | EReference reference = triple.getSecond(); | ||
89 | try { | ||
90 | List<EObject> linkedObjects = getLinkingService().getLinkedObjects(triple.getFirst(), reference, | ||
91 | triple.getThird()); | ||
92 | if (linkedObjects.isEmpty()) { | ||
93 | createAndAddDiagnostic(triple); | ||
94 | return null; | ||
95 | } | ||
96 | if (linkedObjects.size() > 1) { | ||
97 | createAndAddAmbiguousReferenceDiagnostic(triple); | ||
98 | return null; | ||
99 | } | ||
100 | EObject result = linkedObjects.get(0); | ||
101 | if (!EcoreUtil2.isAssignableFrom(reference.getEReferenceType(), result.eClass())) { | ||
102 | log.error("An element of type %s is not assignable to the reference %s.%s".formatted( | ||
103 | result.getClass().getName(), reference.getEContainingClass().getName(), reference.getName())); | ||
104 | createAndAddDiagnostic(triple); | ||
105 | return null; | ||
106 | } | ||
107 | removeDiagnostic(triple); | ||
108 | return result; | ||
109 | } catch (CyclicLinkingError e) { | ||
110 | if (e.triple.equals(triple)) { | ||
111 | log.error(e.getMessage(), e); | ||
112 | createAndAddDiagnostic(triple); | ||
113 | return null; | ||
114 | } else { | ||
115 | throw e; | ||
116 | } | ||
117 | } | ||
118 | } | ||
119 | |||
120 | @Override | ||
121 | protected EObject handleCyclicResolution(Triple<EObject, EReference, INode> triple) throws AssertionError { | ||
122 | // Throw our own version of {@link LazyLinkingResource.CyclicLinkingException}. | ||
123 | throw new CyclicLinkingError("Cyclic resolution of lazy links : %s in resource '%s'.".formatted( | ||
124 | getReferences(triple, resolving), getURI()), triple); | ||
125 | } | ||
126 | |||
127 | @Override | ||
128 | protected void createAndAddDiagnostic(Triple<EObject, EReference, INode> triple) { | ||
129 | if (isValidationDisabled()) { | ||
130 | return; | ||
131 | } | ||
132 | DiagnosticMessage message = createDiagnosticMessage(triple); | ||
133 | addOrReplaceDiagnostic(triple, message); | ||
134 | } | ||
135 | |||
136 | @Override | ||
137 | protected void createAndAddDiagnostic(Triple<EObject, EReference, INode> triple, IllegalNodeException ex) { | ||
138 | if (isValidationDisabled()) { | ||
139 | return; | ||
140 | } | ||
141 | ILinkingDiagnosticMessageProvider.ILinkingDiagnosticContext context = createDiagnosticMessageContext(triple); | ||
142 | DiagnosticMessage message = linkingDiagnosticMessageProvider.getIllegalNodeMessage(context, ex); | ||
143 | addOrReplaceDiagnostic(triple, message); | ||
144 | } | ||
145 | |||
146 | protected void createAndAddAmbiguousReferenceDiagnostic(Triple<EObject, EReference, INode> triple) { | ||
147 | if (isValidationDisabled()) { | ||
148 | return; | ||
149 | } | ||
150 | var context = createDiagnosticMessageContext(triple); | ||
151 | var typeName = context.getReference().getEReferenceType().getName(); | ||
152 | String linkText = ""; | ||
153 | try { | ||
154 | linkText = context.getLinkText(); | ||
155 | } catch (IllegalNodeException e) { | ||
156 | linkText = e.getNode().getText(); | ||
157 | } | ||
158 | var messageString = "Ambiguous reference to %s '%s'.".formatted(typeName, linkText); | ||
159 | var message = new DiagnosticMessage(messageString, Severity.ERROR, | ||
160 | org.eclipse.xtext.diagnostics.Diagnostic.LINKING_DIAGNOSTIC); | ||
161 | addOrReplaceDiagnostic(triple, message); | ||
162 | } | ||
163 | |||
164 | /** | ||
165 | * Adds a diagnostic message while maintaining the invariant that at most one | ||
166 | * {@link ProblemResourceLinkingDiagnostic} is added to the {@link #getErrors()} list. | ||
167 | * | ||
168 | * @param triple The triple to add the diagnostic for. | ||
169 | * @param message The diagnostic message. Must have {@link Severity#ERROR}. | ||
170 | */ | ||
171 | protected void addOrReplaceDiagnostic(Triple<EObject, EReference, INode> triple, DiagnosticMessage message) { | ||
172 | if (message == null) { | ||
173 | return; | ||
174 | } | ||
175 | if (message.getSeverity() != Severity.ERROR) { | ||
176 | throw new IllegalArgumentException("Only linking diagnostics of ERROR severity are supported"); | ||
177 | } | ||
178 | var list = getDiagnosticList(message); | ||
179 | var iterator = list.iterator(); | ||
180 | while (iterator.hasNext()) { | ||
181 | var diagnostic = iterator.next(); | ||
182 | if (diagnostic instanceof ProblemResourceLinkingDiagnostic linkingDiagnostic && | ||
183 | linkingDiagnostic.matchesNode(triple.getThird())) { | ||
184 | if (linkingDiagnostic.matchesMessage(message)) { | ||
185 | return; | ||
186 | } | ||
187 | iterator.remove(); | ||
188 | break; | ||
189 | } | ||
190 | } | ||
191 | var diagnostic = createDiagnostic(triple, message); | ||
192 | list.add(diagnostic); | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Removes the {@link ProblemResourceLinkingDiagnostic} corresponding to the given node, if prevesent, from the | ||
197 | * {@link #getErrors()} list. | ||
198 | * | ||
199 | * @param triple The triple to add the diagnostic for. | ||
200 | */ | ||
201 | @Override | ||
202 | protected void removeDiagnostic(Triple<EObject, EReference, INode> triple) { | ||
203 | if (getErrors().isEmpty()) { | ||
204 | return; | ||
205 | } | ||
206 | var list = getErrors(); | ||
207 | if (list.isEmpty()) { | ||
208 | return; | ||
209 | } | ||
210 | var iterator = list.iterator(); | ||
211 | while (iterator.hasNext()) { | ||
212 | var diagnostic = iterator.next(); | ||
213 | if (diagnostic instanceof ProblemResourceLinkingDiagnostic linkingDiagnostic && | ||
214 | linkingDiagnostic.matchesNode(triple.getThird())) { | ||
215 | iterator.remove(); | ||
216 | return; | ||
217 | } | ||
218 | } | ||
219 | } | ||
220 | |||
221 | @Override | ||
222 | protected Diagnostic createDiagnostic(Triple<EObject, EReference, INode> triple, DiagnosticMessage message) { | ||
223 | return new ProblemResourceLinkingDiagnostic(triple.getThird(), message.getMessage(), | ||
224 | message.getIssueCode(), message.getIssueData()); | ||
225 | } | ||
226 | |||
227 | /** | ||
228 | * Our own version of {@link LazyLinkingResource.CyclicLinkingException}, because the {@code tripe} field in the | ||
229 | * original one is not accessible. | ||
230 | * <p> | ||
231 | * Renamed from {@code CyclicLinkingException} to satisfy naming conventions enforced by Sonar. | ||
232 | */ | ||
233 | public static final class CyclicLinkingError extends AssertionError { | ||
234 | private final transient Triple<EObject, EReference, INode> triple; | ||
235 | |||
236 | private CyclicLinkingError(Object detailMessage, Triple<EObject, EReference, INode> triple) { | ||
237 | super(detailMessage); | ||
238 | this.triple = triple; | ||
239 | } | ||
240 | } | ||
241 | |||
242 | /** | ||
243 | * Marks all diagnostics inserted by {@link ProblemResource} with a common superclass so that they can | ||
244 | * later be removed. | ||
245 | * <p> | ||
246 | * We have to inherit from {@link XtextLinkingDiagnostic} to access the protected function {@link #getNode()}. | ||
247 | */ | ||
248 | protected static class ProblemResourceLinkingDiagnostic extends XtextLinkingDiagnostic { | ||
249 | public ProblemResourceLinkingDiagnostic(INode node, String message, String code, String... data) { | ||
250 | super(node, message, code, data); | ||
251 | } | ||
252 | |||
253 | public boolean matchesNode(INode node) { | ||
254 | return Objects.equals(getNode(), node); | ||
255 | } | ||
256 | |||
257 | public boolean matchesMessage(DiagnosticMessage message) { | ||
258 | return Objects.equals(getMessage(), message.getMessage()) && | ||
259 | Objects.equals(getCode(), message.getIssueCode()) && | ||
260 | Arrays.equals(getData(), message.getIssueData()); | ||
261 | } | ||
262 | } | ||
263 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java index a16f77eb..cac1f265 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java | |||
@@ -24,8 +24,9 @@ import java.util.Map; | |||
24 | 24 | ||
25 | @Singleton | 25 | @Singleton |
26 | public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { | 26 | public class ProblemResourceDescriptionStrategy extends DefaultResourceDescriptionStrategy { |
27 | public static final String ERROR_PREDICATE = "tools.refinery.language.resource" + | 27 | private static final String DATA_PREFIX = "tools.refinery.language.resource.ProblemResourceDescriptionStrategy."; |
28 | ".ProblemResourceDescriptionStrategy.ERROR_PREDICATE"; | 28 | public static final String ARITY = DATA_PREFIX + "ARITY"; |
29 | public static final String ERROR_PREDICATE = DATA_PREFIX + "ERROR_PREDICATE"; | ||
29 | public static final String ERROR_PREDICATE_TRUE = "true"; | 30 | public static final String ERROR_PREDICATE_TRUE = "true"; |
30 | 31 | ||
31 | @Inject | 32 | @Inject |
@@ -97,6 +98,10 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
97 | 98 | ||
98 | protected Map<String, String> getUserData(EObject eObject) { | 99 | protected Map<String, String> getUserData(EObject eObject) { |
99 | var builder = ImmutableMap.<String, String>builder(); | 100 | var builder = ImmutableMap.<String, String>builder(); |
101 | if (eObject instanceof Relation relation) { | ||
102 | int arity = ProblemUtil.getArity(relation); | ||
103 | builder.put(ARITY, Integer.toString(arity)); | ||
104 | } | ||
100 | if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) { | 105 | if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) { |
101 | builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE); | 106 | builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE); |
102 | } | 107 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java index cf099aba..a4437ba6 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemScopeProvider.java | |||
@@ -44,7 +44,7 @@ public class ProblemScopeProvider extends AbstractProblemScopeProvider { | |||
44 | return getVariableScope(context, scope); | 44 | return getVariableScope(context, scope); |
45 | } | 45 | } |
46 | if (reference == ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) { | 46 | if (reference == ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE) { |
47 | return getOppositeScope(context, scope); | 47 | return getOppositeScope(context); |
48 | } | 48 | } |
49 | return scope; | 49 | return scope; |
50 | } | 50 | } |
@@ -96,16 +96,19 @@ public class ProblemScopeProvider extends AbstractProblemScopeProvider { | |||
96 | } | 96 | } |
97 | } | 97 | } |
98 | 98 | ||
99 | protected IScope getOppositeScope(EObject context, IScope delegateScope) { | 99 | protected IScope getOppositeScope(EObject context) { |
100 | var referenceDeclaration = EcoreUtil2.getContainerOfType(context, ReferenceDeclaration.class); | 100 | var referenceDeclaration = EcoreUtil2.getContainerOfType(context, ReferenceDeclaration.class); |
101 | if (referenceDeclaration == null) { | 101 | if (referenceDeclaration == null) { |
102 | return delegateScope; | 102 | return IScope.NULLSCOPE; |
103 | } | 103 | } |
104 | var relation = referenceDeclaration.getReferenceType(); | 104 | var relation = referenceDeclaration.getReferenceType(); |
105 | if (!(relation instanceof ClassDeclaration classDeclaration)) { | 105 | if (!(relation instanceof ClassDeclaration classDeclaration)) { |
106 | return delegateScope; | 106 | return IScope.NULLSCOPE; |
107 | } | 107 | } |
108 | var referenceDeclarations = desugarer.getAllReferenceDeclarations(classDeclaration); | 108 | var referenceDeclarations = classDeclaration.getFeatureDeclarations() |
109 | return Scopes.scopeFor(referenceDeclarations, delegateScope); | 109 | .stream() |
110 | .filter(ReferenceDeclaration.class::isInstance) | ||
111 | .toList(); | ||
112 | return Scopes.scopeFor(referenceDeclarations); | ||
110 | } | 113 | } |
111 | } | 114 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java index a9efc4bb..7b6407e1 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java +++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java | |||
@@ -7,11 +7,12 @@ package tools.refinery.language.utils; | |||
7 | 7 | ||
8 | import org.eclipse.emf.common.util.URI; | 8 | import org.eclipse.emf.common.util.URI; |
9 | import org.eclipse.emf.ecore.EObject; | 9 | import org.eclipse.emf.ecore.EObject; |
10 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
10 | import tools.refinery.language.model.problem.*; | 11 | import tools.refinery.language.model.problem.*; |
11 | 12 | ||
12 | public final class ProblemUtil { | 13 | public final class ProblemUtil { |
13 | public static final String BUILTIN_LIBRARY_NAME = "builtin"; | 14 | public static final String BUILTIN_LIBRARY_NAME = "builtin"; |
14 | public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME); | 15 | public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(); |
15 | 16 | ||
16 | private ProblemUtil() { | 17 | private ProblemUtil() { |
17 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); | 18 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); |
@@ -83,8 +84,44 @@ public final class ProblemUtil { | |||
83 | return true; | 84 | return true; |
84 | } | 85 | } |
85 | 86 | ||
86 | private static URI getLibraryUri(String libraryName) { | 87 | public static int getArity(Relation relation) { |
87 | return URI.createURI(ProblemUtil.class.getClassLoader() | 88 | if (relation instanceof ClassDeclaration || relation instanceof EnumDeclaration) { |
88 | .getResource("tools/refinery/language/%s.problem".formatted(libraryName)).toString()); | 89 | return 1; |
90 | } | ||
91 | if (relation instanceof ReferenceDeclaration) { | ||
92 | return 2; | ||
93 | } | ||
94 | if (relation instanceof PredicateDefinition predicateDefinition) { | ||
95 | return predicateDefinition.getParameters().size(); | ||
96 | } | ||
97 | throw new IllegalArgumentException("Unknown Relation: " + relation); | ||
98 | } | ||
99 | |||
100 | public static boolean isContainerReference(ReferenceDeclaration referenceDeclaration) { | ||
101 | var kind = referenceDeclaration.getKind(); | ||
102 | if (kind == null) { | ||
103 | return false; | ||
104 | } | ||
105 | return switch (kind) { | ||
106 | case CONTAINMENT -> false; | ||
107 | case CONTAINER -> true; | ||
108 | case REFERENCE -> { | ||
109 | var opposite = referenceDeclaration.getOpposite(); | ||
110 | if (opposite == null) { | ||
111 | yield false; | ||
112 | } | ||
113 | opposite = (ReferenceDeclaration) EcoreUtil.resolve(opposite, referenceDeclaration); | ||
114 | yield opposite.getKind() == ReferenceKind.CONTAINMENT; | ||
115 | } | ||
116 | }; | ||
117 | } | ||
118 | |||
119 | private static URI getLibraryUri() { | ||
120 | var libraryResource = ProblemUtil.class.getClassLoader() | ||
121 | .getResource("tools/refinery/language/%s.problem".formatted(BUILTIN_LIBRARY_NAME)); | ||
122 | if (libraryResource == null) { | ||
123 | throw new AssertionError("Library '%s' was not found".formatted(BUILTIN_LIBRARY_NAME)); | ||
124 | } | ||
125 | return URI.createURI(libraryResource.toString()); | ||
89 | } | 126 | } |
90 | } | 127 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java new file mode 100644 index 00000000..0b7cc315 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java | |||
@@ -0,0 +1,31 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.ecore.EObject; | ||
10 | import org.eclipse.emf.ecore.EStructuralFeature; | ||
11 | import org.eclipse.xtext.validation.DiagnosticConverterImpl; | ||
12 | import tools.refinery.language.model.problem.Multiplicity; | ||
13 | import tools.refinery.language.model.problem.ReferenceDeclaration; | ||
14 | import tools.refinery.language.services.ProblemGrammarAccess; | ||
15 | |||
16 | public class ProblemDiagnosticConverter extends DiagnosticConverterImpl { | ||
17 | @Inject | ||
18 | private ProblemGrammarAccess grammarAccess; | ||
19 | |||
20 | @Override | ||
21 | protected IssueLocation getLocationData(EObject obj, EStructuralFeature structuralFeature, int index) { | ||
22 | if (structuralFeature == null && obj instanceof Multiplicity && | ||
23 | obj.eContainer() instanceof ReferenceDeclaration referenceDeclaration) { | ||
24 | // Include the enclosing {@code []} square braces in the error location. | ||
25 | // This lets use have a non-0 length error marker for invalid container references such as | ||
26 | // {@code container Foo[] foo opposite bar}, where unbounded multiplicities are disallowed. | ||
27 | return getLocationData(referenceDeclaration, obj.eContainingFeature()); | ||
28 | } | ||
29 | return super.getLocationData(obj, structuralFeature, index); | ||
30 | } | ||
31 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java index 88d50c5b..8bda4b95 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java +++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java | |||
@@ -9,15 +9,21 @@ | |||
9 | */ | 9 | */ |
10 | package tools.refinery.language.validation; | 10 | package tools.refinery.language.validation; |
11 | 11 | ||
12 | import com.google.inject.Inject; | ||
13 | import org.eclipse.emf.ecore.EObject; | ||
14 | import org.eclipse.emf.ecore.EReference; | ||
12 | import org.eclipse.xtext.EcoreUtil2; | 15 | import org.eclipse.xtext.EcoreUtil2; |
13 | import org.eclipse.xtext.validation.Check; | 16 | import org.eclipse.xtext.validation.Check; |
14 | 17 | import org.jetbrains.annotations.Nullable; | |
15 | import com.google.inject.Inject; | ||
16 | |||
17 | import tools.refinery.language.model.problem.*; | 18 | import tools.refinery.language.model.problem.*; |
18 | import tools.refinery.language.resource.ReferenceCounter; | 19 | import tools.refinery.language.utils.ProblemDesugarer; |
19 | import tools.refinery.language.utils.ProblemUtil; | 20 | import tools.refinery.language.utils.ProblemUtil; |
20 | 21 | ||
22 | import java.util.ArrayList; | ||
23 | import java.util.LinkedHashMap; | ||
24 | import java.util.LinkedHashSet; | ||
25 | import java.util.Set; | ||
26 | |||
21 | /** | 27 | /** |
22 | * This class contains custom validation rules. | 28 | * This class contains custom validation rules. |
23 | * <p> | 29 | * <p> |
@@ -29,13 +35,38 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
29 | 35 | ||
30 | public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; | 36 | public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; |
31 | 37 | ||
32 | public static final String NON_INDIVIDUAL_NODE_ISSUE = ISSUE_PREFIX + "NON_INDIVIDUAL_NODE"; | 38 | public static final String NODE_CONSTANT_ISSUE = ISSUE_PREFIX + "NODE_CONSTANT_ISSUE"; |
39 | |||
40 | public static final String DUPLICATE_NAME_ISSUE = ISSUE_PREFIX + "DUPLICATE_NAME"; | ||
41 | |||
42 | public static final String INVALID_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "INVALID_MULTIPLICITY"; | ||
43 | |||
44 | public static final String ZERO_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "ZERO_MULTIPLICITY"; | ||
45 | |||
46 | public static final String MISSING_OPPOSITE_ISSUE = ISSUE_PREFIX + "MISSING_OPPOSITE"; | ||
47 | |||
48 | public static final String INVALID_OPPOSITE_ISSUE = ISSUE_PREFIX + "INVALID_OPPOSITE"; | ||
49 | |||
50 | public static final String INVALID_SUPERTYPE_ISSUE = ISSUE_PREFIX + "INVALID_SUPERTYPE"; | ||
51 | |||
52 | public static final String INVALID_REFERENCE_TYPE_ISSUE = ISSUE_PREFIX + "INVALID_REFERENCE_TYPE"; | ||
53 | |||
54 | public static final String INVALID_ARITY_ISSUE = ISSUE_PREFIX + "INVALID_ARITY"; | ||
55 | |||
56 | public static final String INVALID_TRANSITIVE_CLOSURE_ISSUE = ISSUE_PREFIX + "INVALID_TRANSITIVE_CLOSURE"; | ||
57 | |||
58 | public static final String INVALID_VALUE_ISSUE = ISSUE_PREFIX + "INVALID_VALUE"; | ||
59 | |||
60 | public static final String UNSUPPORTED_ASSERTION_ISSUE = ISSUE_PREFIX + "UNSUPPORTED_ASSERTION"; | ||
33 | 61 | ||
34 | @Inject | 62 | @Inject |
35 | private ReferenceCounter referenceCounter; | 63 | private ReferenceCounter referenceCounter; |
36 | 64 | ||
65 | @Inject | ||
66 | private ProblemDesugarer desugarer; | ||
67 | |||
37 | @Check | 68 | @Check |
38 | public void checkUniqueVariable(VariableOrNodeExpr expr) { | 69 | public void checkSingletonVariable(VariableOrNodeExpr expr) { |
39 | var variableOrNode = expr.getVariableOrNode(); | 70 | var variableOrNode = expr.getVariableOrNode(); |
40 | if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable) | 71 | if (variableOrNode instanceof Variable variable && ProblemUtil.isImplicitVariable(variable) |
41 | && !ProblemUtil.isSingletonVariable(variable)) { | 72 | && !ProblemUtil.isSingletonVariable(variable)) { |
@@ -51,14 +82,325 @@ public class ProblemValidator extends AbstractProblemValidator { | |||
51 | } | 82 | } |
52 | 83 | ||
53 | @Check | 84 | @Check |
54 | public void checkNonUniqueNode(VariableOrNodeExpr expr) { | 85 | public void checkNodeConstants(VariableOrNodeExpr expr) { |
55 | var variableOrNode = expr.getVariableOrNode(); | 86 | var variableOrNode = expr.getVariableOrNode(); |
56 | if (variableOrNode instanceof Node node && !ProblemUtil.isIndividualNode(node)) { | 87 | if (variableOrNode instanceof Node node && !ProblemUtil.isIndividualNode(node)) { |
57 | var name = node.getName(); | 88 | var name = node.getName(); |
58 | var message = ("Only individual nodes can be referenced in predicates. " + | 89 | var message = ("Only individuals can be referenced in predicates. " + |
59 | "Mark '%s' as individual with the declaration 'indiv %s.'").formatted(name, name); | 90 | "Mark '%s' as individual with the declaration 'indiv %s.'").formatted(name, name); |
60 | error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, | 91 | error(message, expr, ProblemPackage.Literals.VARIABLE_OR_NODE_EXPR__VARIABLE_OR_NODE, |
61 | INSIGNIFICANT_INDEX, NON_INDIVIDUAL_NODE_ISSUE); | 92 | INSIGNIFICANT_INDEX, NODE_CONSTANT_ISSUE); |
93 | } | ||
94 | } | ||
95 | |||
96 | @Check | ||
97 | public void checkUniqueDeclarations(Problem problem) { | ||
98 | var relations = new ArrayList<Relation>(); | ||
99 | var individuals = new ArrayList<Node>(); | ||
100 | for (var statement : problem.getStatements()) { | ||
101 | if (statement instanceof Relation relation) { | ||
102 | relations.add(relation); | ||
103 | } else if (statement instanceof IndividualDeclaration individualDeclaration) { | ||
104 | individuals.addAll(individualDeclaration.getNodes()); | ||
105 | } | ||
106 | } | ||
107 | checkUniqueSimpleNames(relations); | ||
108 | checkUniqueSimpleNames(individuals); | ||
109 | } | ||
110 | |||
111 | @Check | ||
112 | public void checkUniqueFeatures(ClassDeclaration classDeclaration) { | ||
113 | checkUniqueSimpleNames(classDeclaration.getFeatureDeclarations()); | ||
114 | } | ||
115 | |||
116 | @Check | ||
117 | public void checkUniqueLiterals(EnumDeclaration enumDeclaration) { | ||
118 | checkUniqueSimpleNames(enumDeclaration.getLiterals()); | ||
119 | } | ||
120 | |||
121 | protected void checkUniqueSimpleNames(Iterable<? extends NamedElement> namedElements) { | ||
122 | var names = new LinkedHashMap<String, Set<NamedElement>>(); | ||
123 | for (var namedElement : namedElements) { | ||
124 | var name = namedElement.getName(); | ||
125 | var objectsWithName = names.computeIfAbsent(name, ignored -> new LinkedHashSet<>()); | ||
126 | objectsWithName.add(namedElement); | ||
127 | } | ||
128 | for (var entry : names.entrySet()) { | ||
129 | var objectsWithName = entry.getValue(); | ||
130 | if (objectsWithName.size() <= 1) { | ||
131 | continue; | ||
132 | } | ||
133 | var name = entry.getKey(); | ||
134 | var message = "Duplicate name '%s'.".formatted(name); | ||
135 | for (var namedElement : objectsWithName) { | ||
136 | acceptError(message, namedElement, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, | ||
137 | DUPLICATE_NAME_ISSUE); | ||
138 | } | ||
139 | } | ||
140 | } | ||
141 | |||
142 | @Check | ||
143 | public void checkRangeMultiplicity(RangeMultiplicity rangeMultiplicity) { | ||
144 | int lower = rangeMultiplicity.getLowerBound(); | ||
145 | int upper = rangeMultiplicity.getUpperBound(); | ||
146 | if (upper >= 0 && lower > upper) { | ||
147 | var message = "Multiplicity range [%d..%d] is inconsistent."; | ||
148 | acceptError(message, rangeMultiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE); | ||
149 | } | ||
150 | } | ||
151 | |||
152 | @Check | ||
153 | public void checkReferenceMultiplicity(ReferenceDeclaration referenceDeclaration) { | ||
154 | var multiplicity = referenceDeclaration.getMultiplicity(); | ||
155 | if (multiplicity == null) { | ||
156 | return; | ||
157 | } | ||
158 | if (ProblemUtil.isContainerReference(referenceDeclaration) && ( | ||
159 | !(multiplicity instanceof RangeMultiplicity rangeMultiplicity) || | ||
160 | rangeMultiplicity.getLowerBound() != 0 || | ||
161 | rangeMultiplicity.getUpperBound() != 1)) { | ||
162 | var message = "The only allowed multiplicity for container references is [0..1]"; | ||
163 | acceptError(message, multiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE); | ||
164 | } | ||
165 | if ((multiplicity instanceof ExactMultiplicity exactMultiplicity && | ||
166 | exactMultiplicity.getExactValue() == 0) || | ||
167 | (multiplicity instanceof RangeMultiplicity rangeMultiplicity && | ||
168 | rangeMultiplicity.getLowerBound() == 0 && | ||
169 | rangeMultiplicity.getUpperBound() == 0)) { | ||
170 | var message = "The multiplicity constraint does not allow any reference links"; | ||
171 | acceptWarning(message, multiplicity, null, 0, ZERO_MULTIPLICITY_ISSUE); | ||
172 | } | ||
173 | } | ||
174 | |||
175 | @Check | ||
176 | public void checkOpposite(ReferenceDeclaration referenceDeclaration) { | ||
177 | var opposite = referenceDeclaration.getOpposite(); | ||
178 | if (opposite == null || opposite.eIsProxy()) { | ||
179 | return; | ||
180 | } | ||
181 | var oppositeOfOpposite = opposite.getOpposite(); | ||
182 | if (oppositeOfOpposite == null) { | ||
183 | acceptError("Reference '%s' does not declare '%s' as an opposite." | ||
184 | .formatted(opposite.getName(), referenceDeclaration.getName()), | ||
185 | referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, | ||
186 | INVALID_OPPOSITE_ISSUE); | ||
187 | var oppositeResource = opposite.eResource(); | ||
188 | if (oppositeResource != null && oppositeResource.equals(referenceDeclaration.eResource())) { | ||
189 | acceptError("Missing opposite '%s' for reference '%s'." | ||
190 | .formatted(referenceDeclaration.getName(), opposite.getName()), | ||
191 | opposite, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE); | ||
192 | } | ||
193 | return; | ||
194 | } | ||
195 | if (!referenceDeclaration.equals(oppositeOfOpposite)) { | ||
196 | var messageBuilder = new StringBuilder() | ||
197 | .append("Expected reference '") | ||
198 | .append(opposite.getName()) | ||
199 | .append("' to have opposite '") | ||
200 | .append(referenceDeclaration.getName()) | ||
201 | .append("'"); | ||
202 | var oppositeOfOppositeName = oppositeOfOpposite.getName(); | ||
203 | if (oppositeOfOppositeName != null) { | ||
204 | messageBuilder.append(", got '") | ||
205 | .append(oppositeOfOppositeName) | ||
206 | .append("' instead"); | ||
207 | } | ||
208 | messageBuilder.append("."); | ||
209 | acceptError(messageBuilder.toString(), referenceDeclaration, | ||
210 | ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, INVALID_OPPOSITE_ISSUE); | ||
211 | } | ||
212 | } | ||
213 | |||
214 | @Check | ||
215 | public void checkContainerOpposite(ReferenceDeclaration referenceDeclaration) { | ||
216 | var kind = referenceDeclaration.getKind(); | ||
217 | var opposite = referenceDeclaration.getOpposite(); | ||
218 | if (opposite != null && opposite.eIsProxy()) { | ||
219 | // If {@code opposite} is a proxy, we have already emitted a linker error. | ||
220 | return; | ||
221 | } | ||
222 | if (kind == ReferenceKind.CONTAINMENT) { | ||
223 | if (opposite != null && opposite.getKind() == ReferenceKind.CONTAINMENT) { | ||
224 | acceptError("Opposite '%s' of containment reference '%s' is not a container reference." | ||
225 | .formatted(opposite.getName(), referenceDeclaration.getName()), | ||
226 | referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, | ||
227 | INVALID_OPPOSITE_ISSUE); | ||
228 | } | ||
229 | } else if (kind == ReferenceKind.CONTAINER) { | ||
230 | if (opposite == null) { | ||
231 | acceptError("Container reference '%s' requires an opposite.".formatted(referenceDeclaration.getName()), | ||
232 | referenceDeclaration, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE); | ||
233 | } else if (opposite.getKind() != ReferenceKind.CONTAINMENT) { | ||
234 | acceptError("Opposite '%s' of container reference '%s' is not a containment reference." | ||
235 | .formatted(opposite.getName(), referenceDeclaration.getName()), | ||
236 | referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, | ||
237 | INVALID_OPPOSITE_ISSUE); | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | @Check | ||
243 | public void checkSupertypes(ClassDeclaration classDeclaration) { | ||
244 | var supertypes = classDeclaration.getSuperTypes(); | ||
245 | int supertypeCount = supertypes.size(); | ||
246 | for (int i = 0; i < supertypeCount; i++) { | ||
247 | var supertype = supertypes.get(i); | ||
248 | if (!supertype.eIsProxy() && !(supertype instanceof ClassDeclaration)) { | ||
249 | var message = "Supertype '%s' of '%s' is not a class." | ||
250 | .formatted(supertype.getName(), classDeclaration.getName()); | ||
251 | acceptError(message, classDeclaration, ProblemPackage.Literals.CLASS_DECLARATION__SUPER_TYPES, i, | ||
252 | INVALID_SUPERTYPE_ISSUE); | ||
253 | } | ||
254 | } | ||
255 | } | ||
256 | |||
257 | @Check | ||
258 | public void checkReferenceType(ReferenceDeclaration referenceDeclaration) { | ||
259 | if (referenceDeclaration.getKind() == ReferenceKind.REFERENCE && | ||
260 | !ProblemUtil.isContainerReference(referenceDeclaration)) { | ||
261 | checkArity(referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE, 1); | ||
262 | return; | ||
263 | } | ||
264 | var referenceType = referenceDeclaration.getReferenceType(); | ||
265 | if (referenceType == null || referenceType.eIsProxy() || referenceType instanceof ClassDeclaration) { | ||
266 | // Either correct, or a missing reference type where we are probably already emitting another error. | ||
267 | return; | ||
268 | } | ||
269 | var message = "Reference type '%s' of the containment or container reference '%s' is not a class." | ||
270 | .formatted(referenceType.getName(), referenceDeclaration.getName()); | ||
271 | acceptError(message, referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__REFERENCE_TYPE, 0, | ||
272 | INVALID_REFERENCE_TYPE_ISSUE); | ||
273 | } | ||
274 | |||
275 | @Check | ||
276 | public void checkParameterType(Parameter parameter) { | ||
277 | checkArity(parameter, ProblemPackage.Literals.PARAMETER__PARAMETER_TYPE, 1); | ||
278 | } | ||
279 | |||
280 | @Check | ||
281 | public void checkAtom(Atom atom) { | ||
282 | int argumentCount = atom.getArguments().size(); | ||
283 | checkArity(atom, ProblemPackage.Literals.ATOM__RELATION, argumentCount); | ||
284 | if (atom.isTransitiveClosure() && argumentCount != 2) { | ||
285 | var message = "Transitive closure needs exactly 2 arguments, got %d arguments instead." | ||
286 | .formatted(argumentCount); | ||
287 | acceptError(message, atom, ProblemPackage.Literals.ATOM__TRANSITIVE_CLOSURE, 0, | ||
288 | INVALID_TRANSITIVE_CLOSURE_ISSUE); | ||
289 | } | ||
290 | } | ||
291 | |||
292 | @Check | ||
293 | public void checkAssertion(Assertion assertion) { | ||
294 | int argumentCount = assertion.getArguments().size(); | ||
295 | if (!(assertion.getValue() instanceof LogicConstant)) { | ||
296 | var message = "Assertion value must be one of 'true', 'false', 'unknown', or 'error'."; | ||
297 | acceptError(message, assertion, ProblemPackage.Literals.ASSERTION__VALUE, 0, INVALID_VALUE_ISSUE); | ||
298 | } | ||
299 | checkArity(assertion, ProblemPackage.Literals.ASSERTION__RELATION, argumentCount); | ||
300 | } | ||
301 | |||
302 | @Check | ||
303 | public void checkTypeScope(TypeScope typeScope) { | ||
304 | checkArity(typeScope, ProblemPackage.Literals.TYPE_SCOPE__TARGET_TYPE, 1); | ||
305 | } | ||
306 | |||
307 | private void checkArity(EObject eObject, EReference reference, int expectedArity) { | ||
308 | var value = eObject.eGet(reference); | ||
309 | if (!(value instanceof Relation relation) || relation.eIsProxy()) { | ||
310 | // Feature does not point to a {@link Relation}, we are probably already emitting another error. | ||
311 | return; | ||
312 | } | ||
313 | int arity = ProblemUtil.getArity(relation); | ||
314 | if (arity == expectedArity) { | ||
315 | return; | ||
316 | } | ||
317 | var message = "Expected symbol '%s' to have arity %d, got arity %d instead." | ||
318 | .formatted(relation.getName(), expectedArity, arity); | ||
319 | acceptError(message, eObject, reference, 0, INVALID_ARITY_ISSUE); | ||
320 | } | ||
321 | |||
322 | @Check | ||
323 | public void checkMultiObjectAssertion(Assertion assertion) { | ||
324 | var builtinSymbolsOption = desugarer.getBuiltinSymbols(assertion); | ||
325 | if (builtinSymbolsOption.isEmpty()) { | ||
326 | return; | ||
327 | } | ||
328 | var builtinSymbols = builtinSymbolsOption.get(); | ||
329 | var relation = assertion.getRelation(); | ||
330 | boolean isExists = builtinSymbols.exists().equals(relation); | ||
331 | boolean isEquals = builtinSymbols.equals().equals(relation); | ||
332 | if ((!isExists && !isEquals) || !(assertion.getValue() instanceof LogicConstant logicConstant)) { | ||
333 | return; | ||
334 | } | ||
335 | var value = logicConstant.getLogicValue(); | ||
336 | if (assertion.isDefault()) { | ||
337 | acceptError("Default assertions for 'exists' and 'equals' are not supported.", assertion, | ||
338 | ProblemPackage.Literals.ASSERTION__DEFAULT, 0, UNSUPPORTED_ASSERTION_ISSUE); | ||
339 | return; | ||
340 | } | ||
341 | if (value == LogicValue.ERROR) { | ||
342 | acceptError("Error assertions for 'exists' and 'equals' are not supported.", assertion, | ||
343 | ProblemPackage.Literals.ASSERTION__DEFAULT, 0, UNSUPPORTED_ASSERTION_ISSUE); | ||
344 | return; | ||
345 | } | ||
346 | if (isExists) { | ||
347 | checkExistsAssertion(assertion, value); | ||
348 | return; | ||
349 | } | ||
350 | checkEqualsAssertion(assertion, value); | ||
351 | } | ||
352 | |||
353 | private void checkExistsAssertion(Assertion assertion, LogicValue value) { | ||
354 | if (value == LogicValue.TRUE || value == LogicValue.UNKNOWN) { | ||
355 | // {@code true} is always a valid value for {@code exists}, while {@code unknown} values may always be | ||
356 | // refined to {@code true} if necessary (e.g., for individual nodes). | ||
357 | return; | ||
358 | } | ||
359 | var arguments = assertion.getArguments(); | ||
360 | if (arguments.isEmpty()) { | ||
361 | // We already report an error on invalid arity. | ||
362 | return; | ||
363 | } | ||
364 | var node = getNodeArgumentForMultiObjectAssertion(arguments.get(0)); | ||
365 | if (node != null && !node.eIsProxy() && ProblemUtil.isIndividualNode(node)) { | ||
366 | acceptError("Individual nodes must exist.", assertion, null, 0, UNSUPPORTED_ASSERTION_ISSUE); | ||
367 | } | ||
368 | } | ||
369 | |||
370 | private void checkEqualsAssertion(Assertion assertion, LogicValue value) { | ||
371 | var arguments = assertion.getArguments(); | ||
372 | if (arguments.size() < 2) { | ||
373 | // We already report an error on invalid arity. | ||
374 | return; | ||
375 | } | ||
376 | var left = getNodeArgumentForMultiObjectAssertion(arguments.get(0)); | ||
377 | var right = getNodeArgumentForMultiObjectAssertion(arguments.get(1)); | ||
378 | if (left == null || left.eIsProxy() || right == null || right.eIsProxy()) { | ||
379 | return; | ||
380 | } | ||
381 | if (left.equals(right)) { | ||
382 | if (value == LogicValue.FALSE || value == LogicValue.ERROR) { | ||
383 | acceptError("A node cannot be necessarily unequal to itself.", assertion, null, 0, | ||
384 | UNSUPPORTED_ASSERTION_ISSUE); | ||
385 | } | ||
386 | } else { | ||
387 | if (value != LogicValue.FALSE) { | ||
388 | acceptError("Equalities between distinct nodes are not supported.", assertion, null, 0, | ||
389 | UNSUPPORTED_ASSERTION_ISSUE); | ||
390 | } | ||
391 | } | ||
392 | } | ||
393 | |||
394 | @Nullable | ||
395 | private Node getNodeArgumentForMultiObjectAssertion(AssertionArgument argument) { | ||
396 | if (argument instanceof WildcardAssertionArgument) { | ||
397 | acceptError("Wildcard arguments for 'exists' are not supported.", argument, null, 0, | ||
398 | UNSUPPORTED_ASSERTION_ISSUE); | ||
399 | return null; | ||
400 | } | ||
401 | if (argument instanceof NodeAssertionArgument nodeAssertionArgument) { | ||
402 | return nodeAssertionArgument.getNode(); | ||
62 | } | 403 | } |
404 | throw new IllegalArgumentException("Unknown assertion argument: " + argument); | ||
63 | } | 405 | } |
64 | } | 406 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java index f1be55ee..55cbd71d 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ReferenceCounter.java +++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ReferenceCounter.java | |||
@@ -3,7 +3,7 @@ | |||
3 | * | 3 | * |
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | package tools.refinery.language.resource; | 6 | package tools.refinery.language.validation; |
7 | 7 | ||
8 | import java.util.HashMap; | 8 | import java.util.HashMap; |
9 | import java.util.Map; | 9 | import java.util.Map; |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java index 96e7cf9c..72d57f54 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java | |||
@@ -51,6 +51,6 @@ class ProblemParsingTest { | |||
51 | ?children(bob, ciri). | 51 | ?children(bob, ciri). |
52 | taxStatus(anne, ADULT). | 52 | taxStatus(anne, ADULT). |
53 | """); | 53 | """); |
54 | assertThat(problem.errors(), empty()); | 54 | assertThat(problem.getResourceErrors(), empty()); |
55 | } | 55 | } |
56 | } | 56 | } |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java new file mode 100644 index 00000000..464c207c --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java | |||
@@ -0,0 +1,98 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.linking; | ||
7 | |||
8 | |||
9 | import com.google.inject.Inject; | ||
10 | import org.eclipse.xtext.diagnostics.Diagnostic; | ||
11 | import org.eclipse.xtext.testing.InjectWith; | ||
12 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
13 | import org.junit.jupiter.api.extension.ExtendWith; | ||
14 | import org.junit.jupiter.params.ParameterizedTest; | ||
15 | import org.junit.jupiter.params.provider.ValueSource; | ||
16 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
17 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
18 | |||
19 | import static org.hamcrest.MatcherAssert.assertThat; | ||
20 | import static org.hamcrest.Matchers.*; | ||
21 | |||
22 | @ExtendWith(InjectionExtension.class) | ||
23 | @InjectWith(ProblemInjectorProvider.class) | ||
24 | class AmbiguousReferenceTest { | ||
25 | @Inject | ||
26 | private ProblemParseHelper parseHelper; | ||
27 | |||
28 | @ParameterizedTest | ||
29 | @ValueSource(strings = {""" | ||
30 | class Foo { | ||
31 | contains Quux quux | ||
32 | } | ||
33 | |||
34 | class Quux. | ||
35 | |||
36 | quux(f, q). | ||
37 | """, """ | ||
38 | class Foo { | ||
39 | contains Quux quux | ||
40 | } | ||
41 | |||
42 | class Quux. | ||
43 | |||
44 | pred example(Foo f, Quux q) <-> quux(f, q). | ||
45 | """, """ | ||
46 | class Foo { | ||
47 | contains Quux quux opposite foo | ||
48 | } | ||
49 | |||
50 | class Bar { | ||
51 | contains Quux quux opposite bar | ||
52 | } | ||
53 | |||
54 | class Quux { | ||
55 | container Foo foo opposite quux | ||
56 | container Bar bar opposite quux | ||
57 | } | ||
58 | """}) | ||
59 | void unambiguousReferenceTest(String text) { | ||
60 | var problem = parseHelper.parse(text); | ||
61 | assertThat(problem.getResourceErrors(), empty()); | ||
62 | } | ||
63 | |||
64 | @ParameterizedTest | ||
65 | @ValueSource(strings = {""" | ||
66 | class Foo { | ||
67 | contains Quux quux | ||
68 | } | ||
69 | |||
70 | class Bar { | ||
71 | contains Quux quux | ||
72 | } | ||
73 | |||
74 | class Quux. | ||
75 | |||
76 | quux(f, q). | ||
77 | """, """ | ||
78 | class Foo { | ||
79 | contains Quux quux | ||
80 | } | ||
81 | |||
82 | class Bar { | ||
83 | contains Quux quux | ||
84 | } | ||
85 | |||
86 | class Quux. | ||
87 | |||
88 | pred example(Foo f, Quuq q) <-> quux(f, q). | ||
89 | """}) | ||
90 | void ambiguousReferenceTest(String text) { | ||
91 | var problem = parseHelper.parse(text); | ||
92 | var errors = problem.getResourceErrors(); | ||
93 | assertThat(problem.getResourceErrors(), hasItem(allOf( | ||
94 | hasProperty("code", is(Diagnostic.LINKING_DIAGNOSTIC)), | ||
95 | hasProperty("message", containsString("'quux'")) | ||
96 | ))); | ||
97 | } | ||
98 | } | ||
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java index ed193e90..a9c5f62a 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java | |||
@@ -31,7 +31,7 @@ class TransitiveClosureParserTest { | |||
31 | var problem = parseHelper.parse(""" | 31 | var problem = parseHelper.parse(""" |
32 | pred foo(a, b) <-> a + (b) > 10. | 32 | pred foo(a, b) <-> a + (b) > 10. |
33 | """); | 33 | """); |
34 | assertThat(problem.errors(), empty()); | 34 | assertThat(problem.getResourceErrors(), empty()); |
35 | var literal = problem.pred("foo").conj(0).lit(0).get(); | 35 | var literal = problem.pred("foo").conj(0).lit(0).get(); |
36 | assertThat(literal, instanceOf(ComparisonExpr.class)); | 36 | assertThat(literal, instanceOf(ComparisonExpr.class)); |
37 | var left = ((ComparisonExpr) literal).getLeft(); | 37 | var left = ((ComparisonExpr) literal).getLeft(); |
@@ -45,7 +45,7 @@ class TransitiveClosureParserTest { | |||
45 | var problem = parseHelper.parse(""" | 45 | var problem = parseHelper.parse(""" |
46 | pred foo(a, b) <-> equals+(a, b). | 46 | pred foo(a, b) <-> equals+(a, b). |
47 | """); | 47 | """); |
48 | assertThat(problem.errors(), empty()); | 48 | assertThat(problem.getResourceErrors(), empty()); |
49 | var literal = problem.pred("foo").conj(0).lit(0).get(); | 49 | var literal = problem.pred("foo").conj(0).lit(0).get(); |
50 | assertThat(literal, instanceOf(Atom.class)); | 50 | assertThat(literal, instanceOf(Atom.class)); |
51 | var atom = (Atom) literal; | 51 | var atom = (Atom) literal; |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java index 68514bfa..56e65550 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java | |||
@@ -42,7 +42,7 @@ class RuleParsingTest { | |||
42 | """ }) | 42 | """ }) |
43 | void simpleTest(String text) { | 43 | void simpleTest(String text) { |
44 | var problem = parseHelper.parse(text); | 44 | var problem = parseHelper.parse(text); |
45 | assertThat(problem.errors(), empty()); | 45 | assertThat(problem.getResourceErrors(), empty()); |
46 | } | 46 | } |
47 | 47 | ||
48 | @Test | 48 | @Test |
@@ -51,7 +51,7 @@ class RuleParsingTest { | |||
51 | pred Person(p). | 51 | pred Person(p). |
52 | rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown. | 52 | rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown. |
53 | """); | 53 | """); |
54 | assertThat(problem.errors(), empty()); | 54 | assertThat(problem.getResourceErrors(), empty()); |
55 | assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable())); | 55 | assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable())); |
56 | assertThat(problem.rule("r").consequent(0).action(0).newVar(), | 56 | assertThat(problem.rule("r").consequent(0).action(0).newVar(), |
57 | equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable())); | 57 | equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable())); |
@@ -63,7 +63,7 @@ class RuleParsingTest { | |||
63 | pred Friend(a, b). | 63 | pred Friend(a, b). |
64 | rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. | 64 | rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. |
65 | """); | 65 | """); |
66 | assertThat(problem.errors(), empty()); | 66 | assertThat(problem.getResourceErrors(), empty()); |
67 | assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(), | 67 | assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(), |
68 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); | 68 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); |
69 | } | 69 | } |
@@ -74,7 +74,7 @@ class RuleParsingTest { | |||
74 | pred Friend(a, b). | 74 | pred Friend(a, b). |
75 | rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. | 75 | rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. |
76 | """); | 76 | """); |
77 | assertThat(problem.errors(), empty()); | 77 | assertThat(problem.getResourceErrors(), empty()); |
78 | assertThat(problem.rule("r").param(1), | 78 | assertThat(problem.rule("r").param(1), |
79 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); | 79 | not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); |
80 | } | 80 | } |
@@ -85,6 +85,6 @@ class RuleParsingTest { | |||
85 | pred Person(p). | 85 | pred Person(p). |
86 | rule r(p1): must Friend(p1, p2) ==> delete p2. | 86 | rule r(p1): must Friend(p1, p2) ==> delete p2. |
87 | """); | 87 | """); |
88 | assertThat(problem.errors(), not(empty())); | 88 | assertThat(problem.getResourceErrors(), not(empty())); |
89 | } | 89 | } |
90 | } | 90 | } |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java index 734bfcd1..e76d2993 100644 --- a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java | |||
@@ -36,7 +36,7 @@ class NodeScopingTest { | |||
36 | var problem = parse(""" | 36 | var problem = parse(""" |
37 | pred predicate({PARAM}node a). | 37 | pred predicate({PARAM}node a). |
38 | """, qualifiedNamePrefix); | 38 | """, qualifiedNamePrefix); |
39 | assertThat(problem.errors(), empty()); | 39 | assertThat(problem.getResourceErrors(), empty()); |
40 | assertThat(problem.pred("predicate").param(0).getParameterType(), | 40 | assertThat(problem.pred("predicate").param(0).getParameterType(), |
41 | equalTo(problem.builtin().findClass("node").get())); | 41 | equalTo(problem.builtin().findClass("node").get())); |
42 | } | 42 | } |
@@ -48,7 +48,7 @@ class NodeScopingTest { | |||
48 | predicate(a, a). | 48 | predicate(a, a). |
49 | ?predicate(a, b). | 49 | ?predicate(a, b). |
50 | """); | 50 | """); |
51 | assertThat(problem.errors(), empty()); | 51 | assertThat(problem.getResourceErrors(), empty()); |
52 | assertThat(problem.nodeNames(), hasItems("a", "b")); | 52 | assertThat(problem.nodeNames(), hasItems("a", "b")); |
53 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a"))); | 53 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a"))); |
54 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a"))); | 54 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a"))); |
@@ -62,7 +62,7 @@ class NodeScopingTest { | |||
62 | pred predicate(node a) <-> node(b). | 62 | pred predicate(node a) <-> node(b). |
63 | predicate(b). | 63 | predicate(b). |
64 | """); | 64 | """); |
65 | assertThat(problem.errors(), empty()); | 65 | assertThat(problem.getResourceErrors(), empty()); |
66 | assertThat(problem.nodeNames(), hasItems("b")); | 66 | assertThat(problem.nodeNames(), hasItems("b")); |
67 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b"))); | 67 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b"))); |
68 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b"))); | 68 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b"))); |
@@ -77,7 +77,7 @@ class NodeScopingTest { | |||
77 | predicate({PARAM}a, {PARAM}a). | 77 | predicate({PARAM}a, {PARAM}a). |
78 | ?predicate({PARAM}a, {PARAM}b). | 78 | ?predicate({PARAM}a, {PARAM}b). |
79 | """, qualifiedNamePrefix, namedProblem); | 79 | """, qualifiedNamePrefix, namedProblem); |
80 | assertThat(problem.errors(), empty()); | 80 | assertThat(problem.getResourceErrors(), empty()); |
81 | assertThat(problem.nodeNames(), empty()); | 81 | assertThat(problem.nodeNames(), empty()); |
82 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a"))); | 82 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a"))); |
83 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a"))); | 83 | assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a"))); |
@@ -92,7 +92,7 @@ class NodeScopingTest { | |||
92 | indiv b. | 92 | indiv b. |
93 | pred predicate(node a) <-> node({PARAM}b). | 93 | pred predicate(node a) <-> node({PARAM}b). |
94 | """); | 94 | """); |
95 | assertThat(problem.errors(), empty()); | 95 | assertThat(problem.getResourceErrors(), empty()); |
96 | assertThat(problem.nodeNames(), empty()); | 96 | assertThat(problem.nodeNames(), empty()); |
97 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b"))); | 97 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b"))); |
98 | } | 98 | } |
@@ -109,7 +109,7 @@ class NodeScopingTest { | |||
109 | pred predicate(node x) <-> node(x). | 109 | pred predicate(node x) <-> node(x). |
110 | predicate({PARAM}). | 110 | predicate({PARAM}). |
111 | """, qualifiedName); | 111 | """, qualifiedName); |
112 | assertThat(problem.errors(), empty()); | 112 | assertThat(problem.getResourceErrors(), empty()); |
113 | assertThat(problem.nodeNames(), empty()); | 113 | assertThat(problem.nodeNames(), empty()); |
114 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode())); | 114 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode())); |
115 | } | 115 | } |
@@ -121,7 +121,7 @@ class NodeScopingTest { | |||
121 | var problem = parse(""" | 121 | var problem = parse(""" |
122 | pred predicate(node x) <-> node({PARAM}). | 122 | pred predicate(node x) <-> node({PARAM}). |
123 | """, qualifiedName); | 123 | """, qualifiedName); |
124 | assertThat(problem.errors(), empty()); | 124 | assertThat(problem.getResourceErrors(), empty()); |
125 | assertThat(problem.nodeNames(), empty()); | 125 | assertThat(problem.nodeNames(), empty()); |
126 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 126 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
127 | equalTo(problem.builtin().findClass("int").get().getNewNode())); | 127 | equalTo(problem.builtin().findClass("int").get().getNewNode())); |
@@ -139,7 +139,7 @@ class NodeScopingTest { | |||
139 | pred predicate(node x) <-> node(x). | 139 | pred predicate(node x) <-> node(x). |
140 | predicate({PARAM}). | 140 | predicate({PARAM}). |
141 | """, qualifiedName, namedProblem); | 141 | """, qualifiedName, namedProblem); |
142 | assertThat(problem.errors(), empty()); | 142 | assertThat(problem.getResourceErrors(), empty()); |
143 | assertThat(problem.nodeNames(), empty()); | 143 | assertThat(problem.nodeNames(), empty()); |
144 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode())); | 144 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode())); |
145 | } | 145 | } |
@@ -151,7 +151,7 @@ class NodeScopingTest { | |||
151 | class Foo. | 151 | class Foo. |
152 | pred predicate(node x) <-> node({PARAM}). | 152 | pred predicate(node x) <-> node({PARAM}). |
153 | """, qualifiedName, namedProblem); | 153 | """, qualifiedName, namedProblem); |
154 | assertThat(problem.errors(), empty()); | 154 | assertThat(problem.getResourceErrors(), empty()); |
155 | assertThat(problem.nodeNames(), empty()); | 155 | assertThat(problem.nodeNames(), empty()); |
156 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 156 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
157 | equalTo(problem.findClass("Foo").get().getNewNode())); | 157 | equalTo(problem.findClass("Foo").get().getNewNode())); |
@@ -169,7 +169,7 @@ class NodeScopingTest { | |||
169 | pred predicate(node x) <-> node(x). | 169 | pred predicate(node x) <-> node(x). |
170 | predicate(new). | 170 | predicate(new). |
171 | """); | 171 | """); |
172 | assertThat(problem.errors(), empty()); | 172 | assertThat(problem.getResourceErrors(), empty()); |
173 | assertThat(problem.nodeNames(), hasItems("new")); | 173 | assertThat(problem.nodeNames(), hasItems("new")); |
174 | assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode()))); | 174 | assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode()))); |
175 | } | 175 | } |
@@ -182,7 +182,7 @@ class NodeScopingTest { | |||
182 | pred predicate(Foo a) <-> node(a). | 182 | pred predicate(Foo a) <-> node(a). |
183 | predicate({PARAM}). | 183 | predicate({PARAM}). |
184 | """, qualifiedName, namedProblem); | 184 | """, qualifiedName, namedProblem); |
185 | assertThat(problem.errors(), empty()); | 185 | assertThat(problem.getResourceErrors(), empty()); |
186 | assertThat(problem.nodeNames(), empty()); | 186 | assertThat(problem.nodeNames(), empty()); |
187 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha"))); | 187 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha"))); |
188 | } | 188 | } |
@@ -194,7 +194,7 @@ class NodeScopingTest { | |||
194 | enum Foo { alpha, beta } | 194 | enum Foo { alpha, beta } |
195 | pred predicate(Foo a) <-> node({PARAM}). | 195 | pred predicate(Foo a) <-> node({PARAM}). |
196 | """, qualifiedName, namedProblem); | 196 | """, qualifiedName, namedProblem); |
197 | assertThat(problem.errors(), empty()); | 197 | assertThat(problem.getResourceErrors(), empty()); |
198 | assertThat(problem.nodeNames(), empty()); | 198 | assertThat(problem.nodeNames(), empty()); |
199 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 199 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
200 | equalTo(problem.findEnum("Foo").literal("alpha"))); | 200 | equalTo(problem.findEnum("Foo").literal("alpha"))); |
@@ -214,7 +214,7 @@ class NodeScopingTest { | |||
214 | pred predicate(node a) <-> node(a). | 214 | pred predicate(node a) <-> node(a). |
215 | predicate({PARAM}). | 215 | predicate({PARAM}). |
216 | """, qualifiedName); | 216 | """, qualifiedName); |
217 | assertThat(problem.errors(), empty()); | 217 | assertThat(problem.getResourceErrors(), empty()); |
218 | assertThat(problem.nodeNames(), empty()); | 218 | assertThat(problem.nodeNames(), empty()); |
219 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true"))); | 219 | assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true"))); |
220 | } | 220 | } |
@@ -226,7 +226,7 @@ class NodeScopingTest { | |||
226 | var problem = parse(""" | 226 | var problem = parse(""" |
227 | pred predicate() <-> node({PARAM}). | 227 | pred predicate() <-> node({PARAM}). |
228 | """, qualifiedName); | 228 | """, qualifiedName); |
229 | assertThat(problem.errors(), empty()); | 229 | assertThat(problem.getResourceErrors(), empty()); |
230 | assertThat(problem.nodeNames(), empty()); | 230 | assertThat(problem.nodeNames(), empty()); |
231 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), | 231 | assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), |
232 | equalTo(problem.builtin().findEnum("bool").literal("true"))); | 232 | equalTo(problem.builtin().findEnum("bool").literal("true"))); |
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ArityValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ArityValidationTest.java new file mode 100644 index 00000000..68e9fa8d --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/ArityValidationTest.java | |||
@@ -0,0 +1,249 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.Diagnostic; | ||
10 | import org.eclipse.xtext.testing.InjectWith; | ||
11 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
12 | import org.junit.jupiter.api.Test; | ||
13 | import org.junit.jupiter.api.extension.ExtendWith; | ||
14 | import org.junit.jupiter.params.ParameterizedTest; | ||
15 | import org.junit.jupiter.params.provider.Arguments; | ||
16 | import org.junit.jupiter.params.provider.MethodSource; | ||
17 | import org.junit.jupiter.params.provider.ValueSource; | ||
18 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
19 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
20 | import tools.refinery.language.validation.ProblemValidator; | ||
21 | |||
22 | import java.util.stream.Stream; | ||
23 | |||
24 | import static org.hamcrest.MatcherAssert.assertThat; | ||
25 | import static org.hamcrest.Matchers.*; | ||
26 | |||
27 | @ExtendWith(InjectionExtension.class) | ||
28 | @InjectWith(ProblemInjectorProvider.class) | ||
29 | class ArityValidationTest { | ||
30 | @Inject | ||
31 | private ProblemParseHelper parseHelper; | ||
32 | |||
33 | @ParameterizedTest | ||
34 | @ValueSource(strings = {""" | ||
35 | pred Foo(node n) <-> false. | ||
36 | """, """ | ||
37 | pred Foo(node n, node m) <-> false. | ||
38 | """, """ | ||
39 | enum Foo { FOO_A, FOO_B } | ||
40 | """}) | ||
41 | void invalidSupertypeTest(String supertypeDefinition) { | ||
42 | var problem = parseHelper.parse(""" | ||
43 | %s | ||
44 | |||
45 | class Bar extends Foo. | ||
46 | """.formatted(supertypeDefinition)); | ||
47 | var issues = problem.validate(); | ||
48 | assertThat(issues, hasItem(allOf( | ||
49 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
50 | hasProperty("issueCode", is(ProblemValidator.INVALID_SUPERTYPE_ISSUE)), | ||
51 | hasProperty("message", stringContainsInOrder("Foo", "Bar")) | ||
52 | ))); | ||
53 | } | ||
54 | |||
55 | @ParameterizedTest | ||
56 | @ValueSource(strings = {""" | ||
57 | class Foo. | ||
58 | """, """ | ||
59 | abstract class Foo. | ||
60 | """}) | ||
61 | void validSupertypeTest(String supertypeDefinition) { | ||
62 | var problem = parseHelper.parse(""" | ||
63 | %s | ||
64 | |||
65 | class Bar extends Foo. | ||
66 | """.formatted(supertypeDefinition)); | ||
67 | var issues = problem.validate(); | ||
68 | assertThat(issues, empty()); | ||
69 | } | ||
70 | |||
71 | @ParameterizedTest | ||
72 | @ValueSource(strings = {""" | ||
73 | foo(). | ||
74 | """, """ | ||
75 | foo(a1, a2, a3). | ||
76 | """, """ | ||
77 | pred bar() <-> foo(). | ||
78 | """, """ | ||
79 | pred bar(node n) <-> foo(n, n, n). | ||
80 | """, """ | ||
81 | pred bar(foo n) <-> false. | ||
82 | """, """ | ||
83 | scope foo = 1..10. | ||
84 | """, """ | ||
85 | class Bar { | ||
86 | foo[] f | ||
87 | } | ||
88 | """, """ | ||
89 | class Bar { | ||
90 | refers foo[] f | ||
91 | } | ||
92 | """}) | ||
93 | void invalidArityTest(String usage) { | ||
94 | var problem = parseHelper.parse(""" | ||
95 | pred foo(node a, node b) <-> a != b. | ||
96 | |||
97 | %s | ||
98 | """.formatted(usage)); | ||
99 | var issues = problem.validate(); | ||
100 | assertThat(issues, hasItem(allOf( | ||
101 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
102 | hasProperty("issueCode", is(ProblemValidator.INVALID_ARITY_ISSUE)), | ||
103 | hasProperty("message", containsString("foo")) | ||
104 | ))); | ||
105 | } | ||
106 | |||
107 | @ParameterizedTest | ||
108 | @ValueSource(strings = {""" | ||
109 | foo(a). | ||
110 | """, """ | ||
111 | pred bar(node m) <-> !foo(m). | ||
112 | """, """ | ||
113 | pred bar(foo f) <-> true. | ||
114 | """, """ | ||
115 | scope foo = 1..10. | ||
116 | """, """ | ||
117 | class Bar { | ||
118 | foo[] quux | ||
119 | } | ||
120 | """, """ | ||
121 | class Bar { | ||
122 | refers foo[] quux | ||
123 | } | ||
124 | """}) | ||
125 | void validUnaryArityTest(String supertypeDefinition) { | ||
126 | var problem = parseHelper.parse(""" | ||
127 | pred foo(node n) <-> false. | ||
128 | |||
129 | %s | ||
130 | """.formatted(supertypeDefinition)); | ||
131 | var issues = problem.validate(); | ||
132 | assertThat(issues, empty()); | ||
133 | } | ||
134 | |||
135 | @ParameterizedTest | ||
136 | @ValueSource(strings = {""" | ||
137 | foo(a, b). | ||
138 | """, """ | ||
139 | pred bar(node m) <-> !foo(m, m). | ||
140 | """, /* Also test for parameters without any type annotation. */ """ | ||
141 | pred bar(m) <-> foo(m, m). | ||
142 | """}) | ||
143 | void validBinaryArityTest(String supertypeDefinition) { | ||
144 | var problem = parseHelper.parse(""" | ||
145 | pred foo(node n, node m) <-> false. | ||
146 | |||
147 | %s | ||
148 | """.formatted(supertypeDefinition)); | ||
149 | var issues = problem.validate(); | ||
150 | assertThat(issues, empty()); | ||
151 | } | ||
152 | |||
153 | @Test | ||
154 | void notResolvedArityTest() { | ||
155 | var problem = parseHelper.parse(""" | ||
156 | notResolved(a, b). | ||
157 | """); | ||
158 | var issues = problem.validate(); | ||
159 | assertThat(issues, not(contains(hasProperty("issueCode", is(ProblemValidator.INVALID_ARITY_ISSUE))))); | ||
160 | } | ||
161 | |||
162 | @Test | ||
163 | void validTransitiveClosure() { | ||
164 | var problem = parseHelper.parse(""" | ||
165 | pred foo(node a, node b) <-> false. | ||
166 | |||
167 | pred bar(a, b) <-> foo+(a, b). | ||
168 | """); | ||
169 | var issues = problem.validate(); | ||
170 | assertThat(issues, not(contains(hasProperty("issueCode", | ||
171 | is(ProblemValidator.INVALID_TRANSITIVE_CLOSURE_ISSUE))))); | ||
172 | } | ||
173 | |||
174 | @Test | ||
175 | void invalidTransitiveClosure() { | ||
176 | // 0 and 1 argument transitive closures do not get parsed as transitive closure | ||
177 | // due to the ambiguity with the addition operator {@code a + (b)}. | ||
178 | var problem = parseHelper.parse(""" | ||
179 | pred foo(node a, node b) <-> false. | ||
180 | |||
181 | pred bar(node a, node b) <-> foo+(a, b, a). | ||
182 | """); | ||
183 | var issues = problem.validate(); | ||
184 | assertThat(issues, hasItem(allOf( | ||
185 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
186 | hasProperty("issueCode", is(ProblemValidator.INVALID_TRANSITIVE_CLOSURE_ISSUE)) | ||
187 | ))); | ||
188 | } | ||
189 | |||
190 | @ParameterizedTest | ||
191 | @MethodSource | ||
192 | void invalidReferenceTypeTest(String definition, String referenceKind) { | ||
193 | var problem = parseHelper.parse(""" | ||
194 | %s | ||
195 | |||
196 | class Bar { | ||
197 | %s Foo foo | ||
198 | } | ||
199 | """.formatted(definition, referenceKind)); | ||
200 | var issues = problem.validate(); | ||
201 | assertThat(issues, allOf( | ||
202 | hasItem(allOf( | ||
203 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
204 | hasProperty("issueCode", is(ProblemValidator.INVALID_REFERENCE_TYPE_ISSUE)), | ||
205 | hasProperty("message", stringContainsInOrder("Foo", "foo")) | ||
206 | )), | ||
207 | not(hasItem(hasProperty("issueCode", is(ProblemValidator.INVALID_ARITY_ISSUE)))) | ||
208 | )); | ||
209 | } | ||
210 | |||
211 | static Stream<Arguments> invalidReferenceTypeTest() { | ||
212 | return Stream.of( | ||
213 | "pred Foo(node n) <-> true.", | ||
214 | "pred Foo(node n, node m) <-> true.", | ||
215 | "enum Foo { FOO_A, FOO_B }" | ||
216 | ).flatMap(definition -> Stream.of( | ||
217 | Arguments.of(definition, "contains"), | ||
218 | Arguments.of(definition, "container") | ||
219 | )); | ||
220 | } | ||
221 | |||
222 | |||
223 | @ParameterizedTest | ||
224 | @MethodSource | ||
225 | void validReferenceTypeTest(String definition, String referenceKind) { | ||
226 | var problem = parseHelper.parse(""" | ||
227 | %s | ||
228 | |||
229 | class Bar { | ||
230 | %s Foo foo | ||
231 | } | ||
232 | """.formatted(definition, referenceKind)); | ||
233 | var issues = problem.validate(); | ||
234 | assertThat(issues, not(hasItem(hasProperty("issueCode", anyOf( | ||
235 | is(ProblemValidator.INVALID_REFERENCE_TYPE_ISSUE), | ||
236 | is(ProblemValidator.INVALID_ARITY_ISSUE) | ||
237 | ))))); | ||
238 | } | ||
239 | |||
240 | static Stream<Arguments> validReferenceTypeTest() { | ||
241 | return Stream.of( | ||
242 | "class Foo.", | ||
243 | "abstract class Foo." | ||
244 | ).flatMap(definition -> Stream.of( | ||
245 | Arguments.of(definition, "contains"), | ||
246 | Arguments.of(definition, "container") | ||
247 | )); | ||
248 | } | ||
249 | } | ||
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java new file mode 100644 index 00000000..82dea31b --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/AssertionValidationTest.java | |||
@@ -0,0 +1,111 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.xtext.testing.InjectWith; | ||
10 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
11 | import org.junit.jupiter.api.Test; | ||
12 | import org.junit.jupiter.api.extension.ExtendWith; | ||
13 | import org.junit.jupiter.params.ParameterizedTest; | ||
14 | import org.junit.jupiter.params.provider.ValueSource; | ||
15 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
16 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
17 | import tools.refinery.language.validation.ProblemValidator; | ||
18 | |||
19 | import static org.hamcrest.MatcherAssert.assertThat; | ||
20 | import static org.hamcrest.Matchers.*; | ||
21 | import static org.hamcrest.Matchers.is; | ||
22 | |||
23 | @ExtendWith(InjectionExtension.class) | ||
24 | @InjectWith(ProblemInjectorProvider.class) | ||
25 | class AssertionValidationTest { | ||
26 | @Inject | ||
27 | private ProblemParseHelper parseHelper; | ||
28 | |||
29 | @Test | ||
30 | void invalidValueTest() { | ||
31 | var problem = parseHelper.parse(""" | ||
32 | class Foo. | ||
33 | |||
34 | Foo(n): 5. | ||
35 | """); | ||
36 | var issues = problem.validate(); | ||
37 | assertThat(issues, hasItem(hasProperty("issueCode", | ||
38 | is(ProblemValidator.INVALID_VALUE_ISSUE)))); | ||
39 | } | ||
40 | |||
41 | @ParameterizedTest | ||
42 | @ValueSource(strings = { | ||
43 | "default exists(n).", | ||
44 | "!exists(A).", | ||
45 | "exists(A): error.", | ||
46 | "exists(n): error.", | ||
47 | "!exists(*).", | ||
48 | "exists(*): error.", | ||
49 | "default equals(n, n).", | ||
50 | "equals(n, m).", | ||
51 | "?equals(n, m).", | ||
52 | "equals(n, m): error.", | ||
53 | "equals(A, B).", | ||
54 | "?equals(A, B).", | ||
55 | "equals(A, B): error.", | ||
56 | "!equals(n, n).", | ||
57 | "equals(n, n): error.", | ||
58 | "!equals(A, A).", | ||
59 | "equals(A, A): error.", | ||
60 | "?equals(n, *).", | ||
61 | "?equals(*, m).", | ||
62 | "equals(*, *).", | ||
63 | "!equals(*, *).", | ||
64 | "?equals(*, *).", | ||
65 | "equals(*, *): error." | ||
66 | }) | ||
67 | void invalidMultiObjectTest(String assertion) { | ||
68 | var problem = parseHelper.parse(""" | ||
69 | enum Bar { A, B } | ||
70 | |||
71 | %s | ||
72 | """.formatted(assertion)); | ||
73 | var issues = problem.validate(); | ||
74 | assertThat(issues, hasItem(hasProperty("issueCode", | ||
75 | is(ProblemValidator.UNSUPPORTED_ASSERTION_ISSUE)))); | ||
76 | } | ||
77 | |||
78 | @ParameterizedTest | ||
79 | @ValueSource(strings = { | ||
80 | "exists(A).", | ||
81 | "?exists(A).", | ||
82 | "exists(n).", | ||
83 | "?exists(n).", | ||
84 | "!exists(n).", | ||
85 | "exists(*).", | ||
86 | "?exists(*).", | ||
87 | "exists(Foo::new).", | ||
88 | "?exists(Foo::new).", | ||
89 | "!exists(Foo::new).", | ||
90 | "equals(A, A).", | ||
91 | "?equals(A, A).", | ||
92 | "!equals(A, B).", | ||
93 | "equals(n, n).", | ||
94 | "?equals(n, n).", | ||
95 | "!equals(n, m).", | ||
96 | "equals(Foo::new, Foo::new).", | ||
97 | "?equals(Foo::new, Foo::new)." | ||
98 | }) | ||
99 | void validMultiObjectTest(String assertion) { | ||
100 | var problem = parseHelper.parse(""" | ||
101 | class Foo. | ||
102 | |||
103 | enum Bar { A, B } | ||
104 | |||
105 | %s | ||
106 | """.formatted(assertion)); | ||
107 | var issues = problem.validate(); | ||
108 | assertThat(issues, not(hasItem(hasProperty("issueCode", | ||
109 | is(ProblemValidator.UNSUPPORTED_ASSERTION_ISSUE))))); | ||
110 | } | ||
111 | } | ||
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java new file mode 100644 index 00000000..a8bcb1a6 --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java | |||
@@ -0,0 +1,119 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.Diagnostic; | ||
10 | import org.eclipse.xtext.testing.InjectWith; | ||
11 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
12 | import org.junit.jupiter.api.extension.ExtendWith; | ||
13 | import org.junit.jupiter.params.ParameterizedTest; | ||
14 | import org.junit.jupiter.params.provider.ValueSource; | ||
15 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
16 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
17 | import tools.refinery.language.validation.ProblemValidator; | ||
18 | |||
19 | import static org.hamcrest.MatcherAssert.assertThat; | ||
20 | import static org.hamcrest.Matchers.*; | ||
21 | |||
22 | @ExtendWith(InjectionExtension.class) | ||
23 | @InjectWith(ProblemInjectorProvider.class) | ||
24 | class MultiplicityValidationTest { | ||
25 | @Inject | ||
26 | private ProblemParseHelper parseHelper; | ||
27 | |||
28 | @ParameterizedTest | ||
29 | @ValueSource(strings = {"2..5", "2..2", "2..*", "2", ""}) | ||
30 | void validReferenceMultiplicityTest(String range) { | ||
31 | var problem = parseHelper.parse(""" | ||
32 | class Foo { | ||
33 | Bar[%s] bar | ||
34 | } | ||
35 | |||
36 | class Bar. | ||
37 | """.formatted(range)); | ||
38 | assertThat(problem.validate(), empty()); | ||
39 | } | ||
40 | |||
41 | @ParameterizedTest | ||
42 | @ValueSource(strings = {"2..5", "2..2", "2..*", "2", "0..0", "0"}) | ||
43 | void validScopeMultiplicityTest(String range) { | ||
44 | var problem = parseHelper.parse(""" | ||
45 | class Foo. | ||
46 | |||
47 | scope Foo = %s. | ||
48 | """.formatted(range)); | ||
49 | assertThat(problem.validate(), empty()); | ||
50 | } | ||
51 | |||
52 | |||
53 | @ParameterizedTest | ||
54 | @ValueSource(strings = {"0", "0..0"}) | ||
55 | void zeroMReferenceMultiplicityTest(String range) { | ||
56 | var problem = parseHelper.parse(""" | ||
57 | class Foo { | ||
58 | Bar[%s] bar | ||
59 | } | ||
60 | |||
61 | class Bar. | ||
62 | """.formatted(range)); | ||
63 | assertThat(problem.validate(), hasItem(allOf( | ||
64 | hasProperty("severity", is(Diagnostic.WARNING)), | ||
65 | hasProperty("issueCode", is(ProblemValidator.ZERO_MULTIPLICITY_ISSUE)) | ||
66 | ))); | ||
67 | } | ||
68 | |||
69 | @ParameterizedTest | ||
70 | @ValueSource(strings = { | ||
71 | "container Bar bar opposite foo", | ||
72 | "container Bar[0..1] bar opposite foo", | ||
73 | "container Bar bar", // Invalid, but has valid multiplicity. | ||
74 | "container Bar[0..1] bar", // Invalid, but has valid multiplicity. | ||
75 | "Bar bar opposite foo", | ||
76 | "Bar[0..1] bar opposite foo" | ||
77 | }) | ||
78 | void validContainerReference(String referenceText) { | ||
79 | var problem = parseHelper.parse(""" | ||
80 | class Foo { | ||
81 | %s | ||
82 | } | ||
83 | |||
84 | class Bar { | ||
85 | contains Foo foo opposite bar | ||
86 | } | ||
87 | """.formatted(referenceText)); | ||
88 | assertThat(problem.validate(), not(hasItem(hasProperty("issueCode", | ||
89 | is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE))))); | ||
90 | } | ||
91 | |||
92 | @ParameterizedTest | ||
93 | @ValueSource(strings = { | ||
94 | "container Bar[1] bar opposite foo", | ||
95 | "container Bar[1..2] bar opposite foo", | ||
96 | "container Bar[] bar opposite foo", | ||
97 | "container Bar[1] bar", // Also otherwise invalid, because the {@code opposite} is missing. | ||
98 | "container Bar[1..2] bar", // Also otherwise invalid, because the {@code opposite} is missing. | ||
99 | "container Bar[] bar", // Also otherwise invalid, because the {@code opposite} is missing. | ||
100 | "Bar[1] bar opposite foo", | ||
101 | "Bar[1..2] bar opposite foo", | ||
102 | "Bar[] bar opposite foo" | ||
103 | }) | ||
104 | void invalidContainerReference(String referenceText) { | ||
105 | var problem = parseHelper.parse(""" | ||
106 | class Foo { | ||
107 | %s | ||
108 | } | ||
109 | |||
110 | class Bar { | ||
111 | contains Foo foo opposite bar | ||
112 | } | ||
113 | """.formatted(referenceText)); | ||
114 | assertThat(problem.validate(), hasItem(allOf( | ||
115 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
116 | hasProperty("issueCode", is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE)) | ||
117 | ))); | ||
118 | } | ||
119 | } | ||
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java new file mode 100644 index 00000000..57602377 --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java | |||
@@ -0,0 +1,209 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.validation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.Diagnostic; | ||
10 | import org.eclipse.xtext.testing.InjectWith; | ||
11 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
12 | import org.junit.jupiter.api.Test; | ||
13 | import org.junit.jupiter.api.extension.ExtendWith; | ||
14 | import org.junit.jupiter.params.ParameterizedTest; | ||
15 | import org.junit.jupiter.params.provider.ValueSource; | ||
16 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
17 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
18 | import tools.refinery.language.validation.ProblemValidator; | ||
19 | |||
20 | import java.util.Set; | ||
21 | |||
22 | import static org.hamcrest.MatcherAssert.assertThat; | ||
23 | import static org.hamcrest.Matchers.*; | ||
24 | |||
25 | @ExtendWith(InjectionExtension.class) | ||
26 | @InjectWith(ProblemInjectorProvider.class) | ||
27 | class OppositeValidationTest { | ||
28 | @Inject | ||
29 | private ProblemParseHelper parseHelper; | ||
30 | |||
31 | @ParameterizedTest | ||
32 | @ValueSource(strings = {""" | ||
33 | class Foo { | ||
34 | Bar bar opposite foo | ||
35 | } | ||
36 | |||
37 | class Bar { | ||
38 | Foo foo opposite bar | ||
39 | } | ||
40 | """, """ | ||
41 | class Foo { | ||
42 | contains Bar bar opposite foo | ||
43 | } | ||
44 | |||
45 | class Bar { | ||
46 | Foo foo opposite bar | ||
47 | } | ||
48 | """, """ | ||
49 | class Foo { | ||
50 | contains Bar bar opposite foo | ||
51 | } | ||
52 | |||
53 | class Bar { | ||
54 | container Foo foo opposite bar | ||
55 | } | ||
56 | """, """ | ||
57 | class Foo { | ||
58 | Foo foo[] opposite foo | ||
59 | } | ||
60 | """}) | ||
61 | void validOppositeTest(String text) { | ||
62 | var problem = parseHelper.parse(text); | ||
63 | var issues = problem.validate(); | ||
64 | assertThat(issues, not(hasItems(hasProperty("issueCode", in(Set.of( | ||
65 | ProblemValidator.INVALID_OPPOSITE_ISSUE, | ||
66 | ProblemValidator.MISSING_OPPOSITE_ISSUE | ||
67 | )))))); | ||
68 | } | ||
69 | |||
70 | @Test | ||
71 | void missingOppositeTest() { | ||
72 | var problem = parseHelper.parse(""" | ||
73 | class Foo { | ||
74 | Bar bar opposite foo | ||
75 | } | ||
76 | |||
77 | class Bar { | ||
78 | Foo foo | ||
79 | } | ||
80 | """); | ||
81 | var issues = problem.validate(); | ||
82 | assertThat(issues, hasItems(allOf( | ||
83 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
84 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
85 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
86 | ), allOf( | ||
87 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
88 | hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)), | ||
89 | hasProperty("message", stringContainsInOrder("bar", "foo")) | ||
90 | ))); | ||
91 | } | ||
92 | |||
93 | @Test | ||
94 | void oppositeMismatchTest() { | ||
95 | var problem = parseHelper.parse(""" | ||
96 | class Foo { | ||
97 | Bar bar opposite foo | ||
98 | Bar quux opposite foo | ||
99 | } | ||
100 | |||
101 | class Bar { | ||
102 | Foo foo opposite bar | ||
103 | } | ||
104 | """); | ||
105 | var issues = problem.validate(); | ||
106 | assertThat(issues, hasItem(allOf( | ||
107 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
108 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
109 | hasProperty("message", stringContainsInOrder("foo", "quux", "bar")) | ||
110 | ))); | ||
111 | } | ||
112 | |||
113 | @Test | ||
114 | void oppositeMismatchProxyTest() { | ||
115 | var problem = parseHelper.parse(""" | ||
116 | class Foo { | ||
117 | Bar bar opposite foo | ||
118 | } | ||
119 | |||
120 | class Bar { | ||
121 | Foo foo opposite quux | ||
122 | } | ||
123 | """); | ||
124 | var issues = problem.validate(); | ||
125 | assertThat(issues, hasItem(allOf( | ||
126 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
127 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
128 | hasProperty("message", allOf( | ||
129 | stringContainsInOrder("foo", "bar"), | ||
130 | not(containsString("null")) | ||
131 | )) | ||
132 | ))); | ||
133 | } | ||
134 | |||
135 | @ParameterizedTest | ||
136 | @ValueSource(strings = {"contains", "container"}) | ||
137 | void containmentWithProxyOppositeTest(String keyword) { | ||
138 | var problem = parseHelper.parse(""" | ||
139 | class Foo { | ||
140 | %s Bar bar opposite foo | ||
141 | } | ||
142 | |||
143 | class Bar. | ||
144 | """.formatted(keyword)); | ||
145 | var issues = problem.validate(); | ||
146 | assertThat(issues, not(hasItem(hasProperty("issueCode", | ||
147 | is(ProblemValidator.INVALID_OPPOSITE_ISSUE))))); | ||
148 | } | ||
149 | |||
150 | @Test | ||
151 | void containmentWithContainmentOppositeTest() { | ||
152 | var problem = parseHelper.parse(""" | ||
153 | class Foo { | ||
154 | contains Bar bar opposite foo | ||
155 | } | ||
156 | |||
157 | class Bar { | ||
158 | contains Foo foo opposite bar | ||
159 | } | ||
160 | """); | ||
161 | var issues = problem.validate(); | ||
162 | assertThat(issues, hasItems(allOf( | ||
163 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
164 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
165 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
166 | ), allOf( | ||
167 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
168 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
169 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
170 | ))); | ||
171 | } | ||
172 | |||
173 | @Test | ||
174 | void containerWithoutOppositeTest() { | ||
175 | var problem = parseHelper.parse(""" | ||
176 | class Foo { | ||
177 | container Bar bar | ||
178 | } | ||
179 | |||
180 | class Bar. | ||
181 | """); | ||
182 | var issues = problem.validate(); | ||
183 | assertThat(issues, hasItem(allOf( | ||
184 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
185 | hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)), | ||
186 | hasProperty("message", containsString("bar")) | ||
187 | ))); | ||
188 | } | ||
189 | |||
190 | @ParameterizedTest | ||
191 | @ValueSource(strings = {"Foo foo", "container Foo foo"}) | ||
192 | void containerInvalidOppositeTest(String reference) { | ||
193 | var problem = parseHelper.parse(""" | ||
194 | class Foo { | ||
195 | container Bar bar opposite foo | ||
196 | } | ||
197 | |||
198 | class Bar { | ||
199 | %s opposite bar | ||
200 | } | ||
201 | """.formatted(reference)); | ||
202 | var issues = problem.validate(); | ||
203 | assertThat(issues, hasItem(allOf( | ||
204 | hasProperty("severity", is(Diagnostic.ERROR)), | ||
205 | hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)), | ||
206 | hasProperty("message", stringContainsInOrder("foo", "bar")) | ||
207 | ))); | ||
208 | } | ||
209 | } | ||
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java index 6f6a87f7..f1535716 100644 --- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java +++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java | |||
@@ -5,23 +5,26 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.model.tests.utils; | 6 | package tools.refinery.language.model.tests.utils; |
7 | 7 | ||
8 | import org.eclipse.xtext.testing.util.ParseHelper; | ||
9 | |||
10 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
11 | 9 | import org.eclipse.emf.ecore.util.EcoreUtil; | |
10 | import org.eclipse.xtext.testing.util.ParseHelper; | ||
11 | import org.eclipse.xtext.validation.IResourceValidator; | ||
12 | import tools.refinery.language.model.problem.Problem; | 12 | import tools.refinery.language.model.problem.Problem; |
13 | 13 | ||
14 | public class ProblemParseHelper { | 14 | public class ProblemParseHelper { |
15 | @Inject | 15 | @Inject |
16 | private IResourceValidator resourceValidator; | ||
17 | @Inject | ||
16 | private ParseHelper<Problem> parseHelper; | 18 | private ParseHelper<Problem> parseHelper; |
17 | 19 | ||
18 | public WrappedProblem parse(String text) { | 20 | public WrappedProblem parse(String text) { |
19 | Problem problem; | 21 | Problem problem; |
20 | try { | 22 | try { |
21 | problem = parseHelper.parse(text); | 23 | problem = parseHelper.parse(text); |
22 | } catch (Exception e) { | 24 | } catch (Exception e) { |
23 | throw new RuntimeException("Unexpected exception while parsing Problem", e); | 25 | throw new AssertionError("Unexpected exception while parsing Problem", e); |
24 | } | 26 | } |
27 | EcoreUtil.resolveAll(problem); | ||
25 | return new WrappedProblem(problem); | 28 | return new WrappedProblem(problem); |
26 | } | 29 | } |
27 | } | 30 | } |
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java index e5aa0043..fc51ff57 100644 --- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java +++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java | |||
@@ -6,7 +6,7 @@ | |||
6 | package tools.refinery.language.model.tests.utils; | 6 | package tools.refinery.language.model.tests.utils; |
7 | 7 | ||
8 | import org.eclipse.emf.ecore.resource.Resource.Diagnostic; | 8 | import org.eclipse.emf.ecore.resource.Resource.Diagnostic; |
9 | import org.eclipse.emf.ecore.util.EcoreUtil; | 9 | import org.eclipse.emf.ecore.util.Diagnostician; |
10 | import tools.refinery.language.model.problem.*; | 10 | import tools.refinery.language.model.problem.*; |
11 | import tools.refinery.language.utils.BuiltinSymbols; | 11 | import tools.refinery.language.utils.BuiltinSymbols; |
12 | import tools.refinery.language.utils.ProblemDesugarer; | 12 | import tools.refinery.language.utils.ProblemDesugarer; |
@@ -19,11 +19,18 @@ public record WrappedProblem(Problem problem) { | |||
19 | return problem; | 19 | return problem; |
20 | } | 20 | } |
21 | 21 | ||
22 | public List<Diagnostic> errors() { | 22 | public List<Diagnostic> getResourceErrors() { |
23 | EcoreUtil.resolveAll(problem); | ||
24 | return problem.eResource().getErrors(); | 23 | return problem.eResource().getErrors(); |
25 | } | 24 | } |
26 | 25 | ||
26 | public List<Diagnostic> getResourceWarnings() { | ||
27 | return problem.eResource().getWarnings(); | ||
28 | } | ||
29 | |||
30 | public List<org.eclipse.emf.common.util.Diagnostic> validate() { | ||
31 | return Diagnostician.INSTANCE.validate(problem).getChildren(); | ||
32 | } | ||
33 | |||
27 | public WrappedProblem builtin() { | 34 | public WrappedProblem builtin() { |
28 | return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow()); | 35 | return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow()); |
29 | } | 36 | } |
diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java index 2be92464..702e570f 100644 --- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java +++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/LowerTypeScopePropagator.java | |||
@@ -13,9 +13,12 @@ import tools.refinery.store.query.dnf.AnyQuery; | |||
13 | import tools.refinery.store.query.dnf.Query; | 13 | import tools.refinery.store.query.dnf.Query; |
14 | import tools.refinery.store.query.dnf.RelationalQuery; | 14 | import tools.refinery.store.query.dnf.RelationalQuery; |
15 | import tools.refinery.store.query.term.Variable; | 15 | import tools.refinery.store.query.term.Variable; |
16 | import tools.refinery.store.query.term.uppercardinality.UpperCardinalityTerms; | ||
16 | import tools.refinery.store.reasoning.ReasoningBuilder; | 17 | import tools.refinery.store.reasoning.ReasoningBuilder; |
17 | import tools.refinery.store.reasoning.literal.CountCandidateLowerBoundLiteral; | 18 | import tools.refinery.store.reasoning.literal.CountCandidateLowerBoundLiteral; |
19 | import tools.refinery.store.reasoning.literal.CountUpperBoundLiteral; | ||
18 | import tools.refinery.store.reasoning.representation.PartialRelation; | 20 | import tools.refinery.store.reasoning.representation.PartialRelation; |
21 | import tools.refinery.store.representation.cardinality.UpperCardinality; | ||
19 | 22 | ||
20 | import java.util.Collection; | 23 | import java.util.Collection; |
21 | import java.util.List; | 24 | import java.util.List; |
@@ -39,7 +42,7 @@ class LowerTypeScopePropagator extends TypeScopePropagator { | |||
39 | constraint.setLb((lowerBound - getSingleCount())); | 42 | constraint.setLb((lowerBound - getSingleCount())); |
40 | } | 43 | } |
41 | 44 | ||
42 | public static class Factory extends TypeScopePropagator.Factory { | 45 | static class Factory extends TypeScopePropagator.Factory { |
43 | private final PartialRelation type; | 46 | private final PartialRelation type; |
44 | private final int lowerBound; | 47 | private final int lowerBound; |
45 | private final RelationalQuery allMay; | 48 | private final RelationalQuery allMay; |
@@ -77,10 +80,18 @@ class LowerTypeScopePropagator extends TypeScopePropagator { | |||
77 | output.assign(sub(constant(lowerBound), candidateLowerBound)), | 80 | output.assign(sub(constant(lowerBound), candidateLowerBound)), |
78 | check(greater(output, constant(0))) | 81 | check(greater(output, constant(0))) |
79 | ))); | 82 | ))); |
83 | var tooFewObjects = Query.of(type.name() + "#tooFew", builder -> builder | ||
84 | .clause(UpperCardinality.class, upperBound -> List.of( | ||
85 | new CountUpperBoundLiteral(upperBound, type, List.of(Variable.of())), | ||
86 | check(UpperCardinalityTerms.less(upperBound, | ||
87 | UpperCardinalityTerms.constant(UpperCardinality.of(lowerBound)))) | ||
88 | ))); | ||
80 | 89 | ||
81 | storeBuilder.getAdapter(ReasoningBuilder.class).objective(Objectives.value(requiredObjects)); | 90 | storeBuilder.getAdapter(ReasoningBuilder.class).objective(Objectives.value(requiredObjects)); |
82 | storeBuilder.tryGetAdapter(DesignSpaceExplorationBuilder.class).ifPresent(dseBuilder -> | 91 | storeBuilder.tryGetAdapter(DesignSpaceExplorationBuilder.class).ifPresent(dseBuilder -> { |
83 | dseBuilder.accept(Criteria.whenNoMatch(requiredObjects))); | 92 | dseBuilder.accept(Criteria.whenNoMatch(requiredObjects)); |
93 | dseBuilder.exclude(Criteria.whenHasMatch(tooFewObjects)); | ||
94 | }); | ||
84 | } | 95 | } |
85 | } | 96 | } |
86 | } | 97 | } |
diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java index bb50656b..193c132c 100644 --- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java +++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/TypeScopePropagator.java | |||
@@ -59,7 +59,7 @@ abstract class TypeScopePropagator { | |||
59 | adapter.markAsChanged(); | 59 | adapter.markAsChanged(); |
60 | } | 60 | } |
61 | 61 | ||
62 | public abstract static class Factory { | 62 | abstract static class Factory { |
63 | public abstract TypeScopePropagator createPropagator(BoundScopePropagator adapter); | 63 | public abstract TypeScopePropagator createPropagator(BoundScopePropagator adapter); |
64 | 64 | ||
65 | protected abstract Collection<AnyQuery> getQueries(); | 65 | protected abstract Collection<AnyQuery> getQueries(); |
diff --git a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java index 4aba5aac..b2f8d39b 100644 --- a/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java +++ b/subprojects/store-reasoning-scope/src/main/java/tools/refinery/store/reasoning/scope/UpperTypeScopePropagator.java | |||
@@ -5,14 +5,24 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.store.reasoning.scope; | 6 | package tools.refinery.store.reasoning.scope; |
7 | 7 | ||
8 | import tools.refinery.store.dse.transition.DesignSpaceExplorationBuilder; | ||
9 | import tools.refinery.store.dse.transition.objectives.Criteria; | ||
10 | import tools.refinery.store.dse.transition.objectives.Objectives; | ||
11 | import tools.refinery.store.model.ModelStoreBuilder; | ||
8 | import tools.refinery.store.query.dnf.AnyQuery; | 12 | import tools.refinery.store.query.dnf.AnyQuery; |
9 | import tools.refinery.store.query.dnf.Query; | 13 | import tools.refinery.store.query.dnf.Query; |
10 | import tools.refinery.store.query.dnf.RelationalQuery; | 14 | import tools.refinery.store.query.dnf.RelationalQuery; |
15 | import tools.refinery.store.query.term.Variable; | ||
16 | import tools.refinery.store.reasoning.ReasoningBuilder; | ||
17 | import tools.refinery.store.reasoning.literal.CountCandidateUpperBoundLiteral; | ||
18 | import tools.refinery.store.reasoning.literal.CountLowerBoundLiteral; | ||
11 | import tools.refinery.store.reasoning.representation.PartialRelation; | 19 | import tools.refinery.store.reasoning.representation.PartialRelation; |
12 | 20 | ||
13 | import java.util.Collection; | 21 | import java.util.Collection; |
14 | import java.util.List; | 22 | import java.util.List; |
15 | 23 | ||
24 | import static tools.refinery.store.query.literal.Literals.check; | ||
25 | import static tools.refinery.store.query.term.int_.IntTerms.*; | ||
16 | import static tools.refinery.store.reasoning.literal.PartialLiterals.must; | 26 | import static tools.refinery.store.reasoning.literal.PartialLiterals.must; |
17 | import static tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator.MULTI_VIEW; | 27 | import static tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator.MULTI_VIEW; |
18 | 28 | ||
@@ -30,12 +40,14 @@ class UpperTypeScopePropagator extends TypeScopePropagator { | |||
30 | constraint.setUb((upperBound - getSingleCount())); | 40 | constraint.setUb((upperBound - getSingleCount())); |
31 | } | 41 | } |
32 | 42 | ||
33 | public static class Factory extends TypeScopePropagator.Factory { | 43 | static class Factory extends TypeScopePropagator.Factory { |
44 | private final PartialRelation type; | ||
34 | private final int upperBound; | 45 | private final int upperBound; |
35 | private final RelationalQuery allMust; | 46 | private final RelationalQuery allMust; |
36 | private final RelationalQuery multiMust; | 47 | private final RelationalQuery multiMust; |
37 | 48 | ||
38 | public Factory(PartialRelation type, int upperBound) { | 49 | public Factory(PartialRelation type, int upperBound) { |
50 | this.type = type; | ||
39 | this.upperBound = upperBound; | 51 | this.upperBound = upperBound; |
40 | allMust = Query.of(type.name() + "#must", (builder, instance) -> builder.clause( | 52 | allMust = Query.of(type.name() + "#must", (builder, instance) -> builder.clause( |
41 | must(type.call(instance)) | 53 | must(type.call(instance)) |
@@ -55,5 +67,28 @@ class UpperTypeScopePropagator extends TypeScopePropagator { | |||
55 | protected Collection<AnyQuery> getQueries() { | 67 | protected Collection<AnyQuery> getQueries() { |
56 | return List.of(allMust, multiMust); | 68 | return List.of(allMust, multiMust); |
57 | } | 69 | } |
70 | |||
71 | @Override | ||
72 | public void configure(ModelStoreBuilder storeBuilder) { | ||
73 | super.configure(storeBuilder); | ||
74 | |||
75 | var excessObjects = Query.of(type.name() + "#excess", Integer.class, (builder, output) -> builder | ||
76 | .clause(Integer.class, candidateUpperBound -> List.of( | ||
77 | new CountCandidateUpperBoundLiteral(candidateUpperBound, type, List.of(Variable.of())), | ||
78 | output.assign(sub(candidateUpperBound, constant(upperBound))), | ||
79 | check(greater(output, constant(0))) | ||
80 | ))); | ||
81 | var tooManyObjects = Query.of(type.name() + "#tooMany", builder -> builder | ||
82 | .clause(Integer.class, lowerBound -> List.of( | ||
83 | new CountLowerBoundLiteral(lowerBound, type, List.of(Variable.of())), | ||
84 | check(greater(lowerBound, constant(upperBound))) | ||
85 | ))); | ||
86 | |||
87 | storeBuilder.getAdapter(ReasoningBuilder.class).objective(Objectives.value(excessObjects)); | ||
88 | storeBuilder.tryGetAdapter(DesignSpaceExplorationBuilder.class).ifPresent(dseBuilder -> { | ||
89 | dseBuilder.accept(Criteria.whenNoMatch(excessObjects)); | ||
90 | dseBuilder.exclude(Criteria.whenHasMatch(tooManyObjects)); | ||
91 | }); | ||
92 | } | ||
58 | } | 93 | } |
59 | } | 94 | } |
diff --git a/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/PredicateScopeTest.java b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/PredicateScopeTest.java new file mode 100644 index 00000000..a2c56a6b --- /dev/null +++ b/subprojects/store-reasoning-scope/src/test/java/tools/refinery/store/reasoning/scope/PredicateScopeTest.java | |||
@@ -0,0 +1,157 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.reasoning.scope; | ||
7 | |||
8 | import org.junit.jupiter.api.Tag; | ||
9 | import org.junit.jupiter.api.Test; | ||
10 | import org.junit.jupiter.params.ParameterizedTest; | ||
11 | import org.junit.jupiter.params.provider.ValueSource; | ||
12 | import tools.refinery.store.dse.propagation.PropagationAdapter; | ||
13 | import tools.refinery.store.dse.strategy.BestFirstStoreManager; | ||
14 | import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter; | ||
15 | import tools.refinery.store.model.ModelStore; | ||
16 | import tools.refinery.store.query.dnf.Query; | ||
17 | import tools.refinery.store.query.interpreter.QueryInterpreterAdapter; | ||
18 | import tools.refinery.store.query.term.Variable; | ||
19 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
20 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | ||
21 | import tools.refinery.store.reasoning.interpretation.PartialInterpretation; | ||
22 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
23 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
24 | import tools.refinery.store.reasoning.seed.ModelSeed; | ||
25 | import tools.refinery.store.reasoning.translator.PartialRelationTranslator; | ||
26 | import tools.refinery.store.reasoning.translator.containment.ContainmentHierarchyTranslator; | ||
27 | import tools.refinery.store.reasoning.translator.metamodel.Metamodel; | ||
28 | import tools.refinery.store.reasoning.translator.metamodel.MetamodelTranslator; | ||
29 | import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; | ||
30 | import tools.refinery.store.reasoning.translator.multiplicity.ConstrainedMultiplicity; | ||
31 | import tools.refinery.store.representation.TruthValue; | ||
32 | import tools.refinery.store.representation.cardinality.CardinalityIntervals; | ||
33 | import tools.refinery.store.statecoding.StateCoderAdapter; | ||
34 | import tools.refinery.store.tuple.Tuple; | ||
35 | |||
36 | import static org.hamcrest.MatcherAssert.assertThat; | ||
37 | import static org.hamcrest.Matchers.is; | ||
38 | import static tools.refinery.store.query.literal.Literals.not; | ||
39 | |||
40 | class PredicateScopeTest { | ||
41 | private static final PartialRelation index = new PartialRelation("Index", 1); | ||
42 | private static final PartialRelation next = new PartialRelation("next", 2); | ||
43 | private static final PartialRelation nextInvalidMultiplicity = new PartialRelation("next::invalidMultiplicity", 1); | ||
44 | private static final PartialRelation prev = new PartialRelation("prev", 2); | ||
45 | private static final PartialRelation prevInvalidMultiplicity = new PartialRelation("prev::invalidMultiplicity", 1); | ||
46 | private static final PartialRelation loop = new PartialRelation("loop", 1); | ||
47 | private static final PartialRelation first = new PartialRelation("first", 1); | ||
48 | private static final PartialRelation last = new PartialRelation("last", 1); | ||
49 | |||
50 | @Tag("slow") | ||
51 | @ParameterizedTest | ||
52 | @ValueSource(ints = {1, 2, 3, 4, 5}) | ||
53 | void generateTest(int randomSeed) { | ||
54 | var store = createStore(); | ||
55 | var newIndex = Tuple.of(0); | ||
56 | var modelSeed = ModelSeed.builder(1) | ||
57 | .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder | ||
58 | .reducedValue(CardinalityIntervals.ONE) | ||
59 | .put(newIndex, CardinalityIntervals.SET)) | ||
60 | .seed(ContainmentHierarchyTranslator.CONTAINED_SYMBOL, | ||
61 | builder -> builder.reducedValue(TruthValue.UNKNOWN)) | ||
62 | .seed(index, builder -> builder | ||
63 | .reducedValue(TruthValue.UNKNOWN) | ||
64 | .put(newIndex, TruthValue.TRUE)) | ||
65 | .seed(next, builder -> builder.reducedValue(TruthValue.UNKNOWN)) | ||
66 | .seed(prev, builder -> builder.reducedValue(TruthValue.UNKNOWN)) | ||
67 | .build(); | ||
68 | var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); | ||
69 | var initialVersion = model.commit(); | ||
70 | var bestFistSearch = new BestFirstStoreManager(store, 1); | ||
71 | bestFistSearch.startExploration(initialVersion, randomSeed); | ||
72 | model.restore(bestFistSearch.getSolutionStore().getSolutions().get(0).version()); | ||
73 | var reasoningAdapter = model.getAdapter(ReasoningAdapter.class); | ||
74 | var firstInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, first); | ||
75 | assertSize(firstInterpretation, 1); | ||
76 | var lastInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, last); | ||
77 | assertSize(lastInterpretation, 1); | ||
78 | } | ||
79 | |||
80 | @Test | ||
81 | void invalidResultTest() { | ||
82 | var store = createStore(); | ||
83 | var modelSeed = ModelSeed.builder(8) | ||
84 | .seed(MultiObjectTranslator.COUNT_SYMBOL, builder -> builder.reducedValue(CardinalityIntervals.ONE)) | ||
85 | .seed(ContainmentHierarchyTranslator.CONTAINED_SYMBOL, | ||
86 | builder -> builder.reducedValue(TruthValue.UNKNOWN)) | ||
87 | .seed(ContainmentHierarchyTranslator.CONTAINS_SYMBOL, | ||
88 | builder -> builder.reducedValue(TruthValue.UNKNOWN)) | ||
89 | .seed(index, builder -> builder.reducedValue(TruthValue.TRUE)) | ||
90 | .seed(next, builder -> builder | ||
91 | .reducedValue(TruthValue.UNKNOWN) | ||
92 | .put(Tuple.of(1, 2), TruthValue.TRUE) | ||
93 | .put(Tuple.of(2, 3), TruthValue.TRUE) | ||
94 | .put(Tuple.of(3, 4), TruthValue.TRUE) | ||
95 | .put(Tuple.of(4, 5), TruthValue.TRUE) | ||
96 | .put(Tuple.of(6, 1), TruthValue.TRUE)) | ||
97 | .seed(prev, builder -> builder.reducedValue(TruthValue.UNKNOWN)) | ||
98 | .build(); | ||
99 | var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); | ||
100 | var reasoningAdapter = model.getAdapter(ReasoningAdapter.class); | ||
101 | var firstInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, first); | ||
102 | assertSize(firstInterpretation, 3); | ||
103 | var lastInterpretation = reasoningAdapter.getPartialInterpretation(Concreteness.CANDIDATE, last); | ||
104 | assertSize(lastInterpretation, 3); | ||
105 | var designSpaceAdapter = model.getAdapter(DesignSpaceExplorationAdapter.class); | ||
106 | assertThat(designSpaceAdapter.checkAccept(), is(false)); | ||
107 | } | ||
108 | |||
109 | private ModelStore createStore() { | ||
110 | var metamodel = Metamodel.builder() | ||
111 | .type(index) | ||
112 | .reference(next, index, false, | ||
113 | ConstrainedMultiplicity.of(CardinalityIntervals.LONE, nextInvalidMultiplicity), index, prev) | ||
114 | .reference(prev, index, false, | ||
115 | ConstrainedMultiplicity.of(CardinalityIntervals.LONE, prevInvalidMultiplicity), index, next) | ||
116 | .build(); | ||
117 | return ModelStore.builder() | ||
118 | .with(QueryInterpreterAdapter.builder()) | ||
119 | .with(StateCoderAdapter.builder()) | ||
120 | .with(PropagationAdapter.builder()) | ||
121 | .with(DesignSpaceExplorationAdapter.builder()) | ||
122 | .with(ReasoningAdapter.builder()) | ||
123 | .with(new MultiObjectTranslator()) | ||
124 | .with(new MetamodelTranslator(metamodel)) | ||
125 | .with(PartialRelationTranslator.of(loop) | ||
126 | .query(Query.of("loop", (builder, p1) -> builder.clause( | ||
127 | index.call(p1), | ||
128 | next.callTransitive(p1, p1) | ||
129 | ))) | ||
130 | .mayNever()) | ||
131 | .with(PartialRelationTranslator.of(first) | ||
132 | .query(Query.of("first", (builder, p1) -> builder.clause( | ||
133 | index.call(p1), | ||
134 | not(prev.call(p1, Variable.of())) | ||
135 | )))) | ||
136 | .with(PartialRelationTranslator.of(last) | ||
137 | .query(Query.of("last", (builder, p1) -> builder.clause( | ||
138 | index.call(p1), | ||
139 | not(next.call(p1, Variable.of())) | ||
140 | )))) | ||
141 | .with(new ScopePropagator() | ||
142 | .scope(index, CardinalityIntervals.exactly(8)) | ||
143 | .scope(first, CardinalityIntervals.ONE) | ||
144 | .scope(last, CardinalityIntervals.ONE)) | ||
145 | .build(); | ||
146 | } | ||
147 | |||
148 | private void assertSize(PartialInterpretation<TruthValue, Boolean> partialInterpretation, int expected) { | ||
149 | int size = 0; | ||
150 | var cursor = partialInterpretation.getAll(); | ||
151 | while (cursor.move()) { | ||
152 | assertThat(cursor.getValue(), is(TruthValue.TRUE)); | ||
153 | size++; | ||
154 | } | ||
155 | assertThat(size, is(expected)); | ||
156 | } | ||
157 | } | ||
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java index 61037be3..c85bd8b7 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentHierarchyTranslator.java | |||
@@ -52,9 +52,10 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { | |||
52 | 52 | ||
53 | private final Symbol<InferredContainment> containsStorage = Symbol.of("CONTAINS", 2, InferredContainment.class, | 53 | private final Symbol<InferredContainment> containsStorage = Symbol.of("CONTAINS", 2, InferredContainment.class, |
54 | InferredContainment.UNKNOWN); | 54 | InferredContainment.UNKNOWN); |
55 | private final AnySymbolView mustAnyContainmentLinkView = new MustAnyContainmentLinkView(containsStorage); | ||
55 | private final AnySymbolView forbiddenContainsView = new ForbiddenContainsView(containsStorage); | 56 | private final AnySymbolView forbiddenContainsView = new ForbiddenContainsView(containsStorage); |
56 | private final RelationalQuery containsMayNewTargetHelper; | 57 | private final RelationalQuery containsMayNewTargetHelper; |
57 | private final RelationalQuery containsMayExistingHelper; | 58 | private final RelationalQuery containsWithoutLink; |
58 | private final RelationalQuery weakComponents; | 59 | private final RelationalQuery weakComponents; |
59 | private final RelationalQuery strongComponents; | 60 | private final RelationalQuery strongComponents; |
60 | private final Map<PartialRelation, ContainmentInfo> containmentInfoMap; | 61 | private final Map<PartialRelation, ContainmentInfo> containmentInfoMap; |
@@ -67,18 +68,15 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { | |||
67 | containsMayNewTargetHelper = Query.of(name + "#mayNewTargetHelper", (builder, child) -> builder | 68 | containsMayNewTargetHelper = Query.of(name + "#mayNewTargetHelper", (builder, child) -> builder |
68 | .clause(Integer.class, existingContainers -> List.of( | 69 | .clause(Integer.class, existingContainers -> List.of( |
69 | may(CONTAINED_SYMBOL.call(child)), | 70 | may(CONTAINED_SYMBOL.call(child)), |
70 | new CountLowerBoundLiteral(existingContainers, CONTAINS_SYMBOL, List.of(Variable.of(), child)), | 71 | new CountLowerBoundLiteral(existingContainers, CONTAINS_SYMBOL, |
72 | List.of(Variable.of(), child)), | ||
71 | check(less(existingContainers, constant(1))) | 73 | check(less(existingContainers, constant(1))) |
72 | ))); | 74 | ))); |
73 | 75 | ||
74 | containsMayExistingHelper = Query.of(name + "#mayExistingHelper", (builder, parent, child) -> builder | 76 | containsWithoutLink = Query.of(name + "#withoutLink", (builder, parent, child) -> builder.clause( |
75 | .clause(Integer.class, existingContainers -> List.of( | 77 | must(CONTAINS_SYMBOL.call(parent, child)), |
76 | must(CONTAINS_SYMBOL.call(parent, child)), | 78 | not(mustAnyContainmentLinkView.call(parent, child)) |
77 | not(forbiddenContainsView.call(parent, child)) | 79 | )); |
78 | // Violation of monotonicity: | ||
79 | // Containment edges violating upper multiplicity will not be marked as {@code ERROR}, but the | ||
80 | // {@code invalidNumberOfContainers} error pattern will already mark the node as invalid. | ||
81 | ))); | ||
82 | 80 | ||
83 | var mustExistBothContains = Query.of(name + "#mustExistBoth", (builder, parent, child) -> builder.clause( | 81 | var mustExistBothContains = Query.of(name + "#mustExistBoth", (builder, parent, child) -> builder.clause( |
84 | must(CONTAINS_SYMBOL.call(parent, child)), | 82 | must(CONTAINS_SYMBOL.call(parent, child)), |
@@ -139,13 +137,21 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { | |||
139 | var mayNewHelper = Query.of(name + "#mayNewHelper", (builder, parent, child) -> builder.clause( | 137 | var mayNewHelper = Query.of(name + "#mayNewHelper", (builder, parent, child) -> builder.clause( |
140 | mayNewSourceHelper.call(parent), | 138 | mayNewSourceHelper.call(parent), |
141 | mayNewTargetHelper.call(child), | 139 | mayNewTargetHelper.call(child), |
142 | not(must(CONTAINS_SYMBOL.call(parent, child))), | 140 | not(mustAnyContainmentLinkView.call(parent, child)), |
143 | not(forbiddenLinkView.call(parent, child)) | 141 | not(forbiddenLinkView.call(parent, child)) |
144 | )); | 142 | )); |
145 | 143 | ||
144 | var existingContainsLink = Query.of(name + "#existingContaints", (builder, parent, child) -> builder | ||
145 | .clause( | ||
146 | must(linkType.call(parent, child)) | ||
147 | ) | ||
148 | .clause( | ||
149 | containsWithoutLink.call(parent, child) | ||
150 | )); | ||
151 | |||
146 | var mayExistingHelper = Query.of(name + "#mayExistingHelper", (builder, parent, child) -> builder.clause( | 152 | var mayExistingHelper = Query.of(name + "#mayExistingHelper", (builder, parent, child) -> builder.clause( |
147 | must(linkType.call(parent, child)), | 153 | existingContainsLink.call(parent, child), |
148 | containsMayExistingHelper.call(parent, child), | 154 | not(forbiddenContainsView.call(parent, child)), |
149 | may(sourceType.call(parent)), | 155 | may(sourceType.call(parent)), |
150 | may(targetType.call(child)), | 156 | may(targetType.call(child)), |
151 | not(forbiddenLinkView.call(parent, child)) | 157 | not(forbiddenLinkView.call(parent, child)) |
@@ -224,7 +230,9 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { | |||
224 | })) | 230 | })) |
225 | .must(Query.of(mustName, (builder, parent, child) -> builder.clause( | 231 | .must(Query.of(mustName, (builder, parent, child) -> builder.clause( |
226 | new MustContainsView(containsStorage).call(parent, child) | 232 | new MustContainsView(containsStorage).call(parent, child) |
227 | )))); | 233 | ))) |
234 | .refiner(ContainsRefiner.of(containsStorage)) | ||
235 | .initializer(new RefinementBasedInitializer<>(CONTAINS_SYMBOL))); | ||
228 | } | 236 | } |
229 | 237 | ||
230 | private void translateInvalidContainer(ModelStoreBuilder storeBuilder) { | 238 | private void translateInvalidContainer(ModelStoreBuilder storeBuilder) { |
@@ -245,7 +253,7 @@ public class ContainmentHierarchyTranslator implements ModelStoreConfiguration { | |||
245 | ) | 253 | ) |
246 | .clause(container -> List.of( | 254 | .clause(container -> List.of( |
247 | MultiObjectTranslator.MULTI_VIEW.call(multi), | 255 | MultiObjectTranslator.MULTI_VIEW.call(multi), |
248 | must(CONTAINS_SYMBOL.call(container, multi)), | 256 | mustAnyContainmentLinkView.call(container, multi), |
249 | not(MultiObjectTranslator.MULTI_VIEW.call(container)) | 257 | not(MultiObjectTranslator.MULTI_VIEW.call(container)) |
250 | )) | 258 | )) |
251 | .action( | 259 | .action( |
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java index 497ed98f..e44fcffd 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainmentLinkRefiner.java | |||
@@ -24,8 +24,8 @@ class ContainmentLinkRefiner extends AbstractPartialInterpretationRefiner<TruthV | |||
24 | private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner; | 24 | private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner; |
25 | private final PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner; | 25 | private final PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner; |
26 | 26 | ||
27 | public ContainmentLinkRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol, | 27 | private ContainmentLinkRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol, |
28 | Factory factory) { | 28 | Factory factory) { |
29 | super(adapter, partialSymbol); | 29 | super(adapter, partialSymbol); |
30 | this.factory = factory; | 30 | this.factory = factory; |
31 | interpretation = adapter.getModel().getInterpretation(factory.symbol); | 31 | interpretation = adapter.getModel().getInterpretation(factory.symbol); |
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainsRefiner.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainsRefiner.java new file mode 100644 index 00000000..a7196a1c --- /dev/null +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/ContainsRefiner.java | |||
@@ -0,0 +1,71 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.reasoning.translator.containment; | ||
7 | |||
8 | import tools.refinery.store.model.Interpretation; | ||
9 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
10 | import tools.refinery.store.reasoning.refinement.AbstractPartialInterpretationRefiner; | ||
11 | import tools.refinery.store.reasoning.refinement.PartialInterpretationRefiner; | ||
12 | import tools.refinery.store.reasoning.representation.PartialSymbol; | ||
13 | import tools.refinery.store.representation.Symbol; | ||
14 | import tools.refinery.store.representation.TruthValue; | ||
15 | import tools.refinery.store.tuple.Tuple; | ||
16 | |||
17 | import java.util.LinkedHashMap; | ||
18 | import java.util.Map; | ||
19 | import java.util.Set; | ||
20 | |||
21 | class ContainsRefiner extends AbstractPartialInterpretationRefiner<TruthValue, Boolean> { | ||
22 | private static final Map<TruthValue, InferredContainment> EMPTY_VALUES; | ||
23 | |||
24 | static { | ||
25 | var values = TruthValue.values(); | ||
26 | EMPTY_VALUES = new LinkedHashMap<>(values.length); | ||
27 | for (var value : values) { | ||
28 | EMPTY_VALUES.put(value, new InferredContainment(value, Set.of(), Set.of())); | ||
29 | } | ||
30 | } | ||
31 | |||
32 | private final Interpretation<InferredContainment> interpretation; | ||
33 | private final PartialInterpretationRefiner<TruthValue, Boolean> containedRefiner; | ||
34 | |||
35 | private ContainsRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol, | ||
36 | Symbol<InferredContainment> containsStorage) { | ||
37 | super(adapter, partialSymbol); | ||
38 | interpretation = adapter.getModel().getInterpretation(containsStorage); | ||
39 | containedRefiner = adapter.getRefiner(ContainmentHierarchyTranslator.CONTAINED_SYMBOL); | ||
40 | } | ||
41 | |||
42 | @Override | ||
43 | public boolean merge(Tuple key, TruthValue value) { | ||
44 | var oldValue = interpretation.get(key); | ||
45 | var newValue = mergeLink(oldValue, value); | ||
46 | if (oldValue != newValue) { | ||
47 | interpretation.put(key, newValue); | ||
48 | } | ||
49 | if (value.must()) { | ||
50 | return containedRefiner.merge(Tuple.of(key.get(1)), TruthValue.TRUE); | ||
51 | } | ||
52 | return true; | ||
53 | } | ||
54 | |||
55 | public InferredContainment mergeLink(InferredContainment oldValue, TruthValue toMerge) { | ||
56 | var newContains = oldValue.contains().merge(toMerge); | ||
57 | if (newContains.equals(oldValue.contains())) { | ||
58 | return oldValue; | ||
59 | } | ||
60 | var mustLinks = oldValue.mustLinks(); | ||
61 | var forbiddenLinks = oldValue.forbiddenLinks(); | ||
62 | if (mustLinks.isEmpty() && forbiddenLinks.isEmpty()) { | ||
63 | return EMPTY_VALUES.get(newContains); | ||
64 | } | ||
65 | return new InferredContainment(newContains, mustLinks, forbiddenLinks); | ||
66 | } | ||
67 | |||
68 | public static PartialInterpretationRefiner.Factory<TruthValue, Boolean> of(Symbol<InferredContainment> symbol) { | ||
69 | return (adapter, partialSymbol) -> new ContainsRefiner(adapter, partialSymbol, symbol); | ||
70 | } | ||
71 | } | ||
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java index 8df23d9a..8a757ed2 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/InferredContainment.java | |||
@@ -24,7 +24,7 @@ final class InferredContainment { | |||
24 | this.contains = adjustContains(contains, mustLinks, forbiddenLinks); | 24 | this.contains = adjustContains(contains, mustLinks, forbiddenLinks); |
25 | this.mustLinks = mustLinks; | 25 | this.mustLinks = mustLinks; |
26 | this.forbiddenLinks = forbiddenLinks; | 26 | this.forbiddenLinks = forbiddenLinks; |
27 | hashCode = Objects.hash(contains, mustLinks, forbiddenLinks); | 27 | hashCode = Objects.hash(this.contains, mustLinks, forbiddenLinks); |
28 | } | 28 | } |
29 | 29 | ||
30 | private static TruthValue adjustContains(TruthValue contains, Set<PartialRelation> mustLinks, | 30 | private static TruthValue adjustContains(TruthValue contains, Set<PartialRelation> mustLinks, |
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/MustAnyContainmentLinkView.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/MustAnyContainmentLinkView.java new file mode 100644 index 00000000..1cc537c6 --- /dev/null +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/containment/MustAnyContainmentLinkView.java | |||
@@ -0,0 +1,21 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.store.reasoning.translator.containment; | ||
7 | |||
8 | import tools.refinery.store.query.view.TuplePreservingView; | ||
9 | import tools.refinery.store.representation.Symbol; | ||
10 | import tools.refinery.store.tuple.Tuple; | ||
11 | |||
12 | class MustAnyContainmentLinkView extends TuplePreservingView<InferredContainment> { | ||
13 | public MustAnyContainmentLinkView(Symbol<InferredContainment> symbol) { | ||
14 | super(symbol, "contains#mustAnyLink"); | ||
15 | } | ||
16 | |||
17 | @Override | ||
18 | protected boolean doFilter(Tuple key, InferredContainment value) { | ||
19 | return !value.mustLinks().isEmpty(); | ||
20 | } | ||
21 | } | ||
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java index 0700f9f7..2e804b44 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/crossreference/DirectedCrossReferenceRefiner.java | |||
@@ -15,15 +15,17 @@ import tools.refinery.store.representation.TruthValue; | |||
15 | import tools.refinery.store.tuple.Tuple; | 15 | import tools.refinery.store.tuple.Tuple; |
16 | 16 | ||
17 | class DirectedCrossReferenceRefiner extends ConcreteSymbolRefiner<TruthValue, Boolean> { | 17 | class DirectedCrossReferenceRefiner extends ConcreteSymbolRefiner<TruthValue, Boolean> { |
18 | private final PartialRelation targetType; | ||
18 | private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner; | 19 | private final PartialInterpretationRefiner<TruthValue, Boolean> sourceRefiner; |
19 | private final PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner; | 20 | private PartialInterpretationRefiner<TruthValue, Boolean> targetRefiner; |
20 | 21 | ||
21 | public DirectedCrossReferenceRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol, | 22 | public DirectedCrossReferenceRefiner(ReasoningAdapter adapter, PartialSymbol<TruthValue, Boolean> partialSymbol, |
22 | Symbol<TruthValue> concreteSymbol, PartialRelation sourceType, | 23 | Symbol<TruthValue> concreteSymbol, PartialRelation sourceType, |
23 | PartialRelation targetType) { | 24 | PartialRelation targetType) { |
24 | super(adapter, partialSymbol, concreteSymbol); | 25 | super(adapter, partialSymbol, concreteSymbol); |
26 | this.targetType = targetType; | ||
27 | // Source is always a class, so we can rely on the fact that it is always constructed before this refiner. | ||
25 | sourceRefiner = adapter.getRefiner(sourceType); | 28 | sourceRefiner = adapter.getRefiner(sourceType); |
26 | targetRefiner = adapter.getRefiner(targetType); | ||
27 | } | 29 | } |
28 | 30 | ||
29 | @Override | 31 | @Override |
@@ -32,6 +34,10 @@ class DirectedCrossReferenceRefiner extends ConcreteSymbolRefiner<TruthValue, Bo | |||
32 | return false; | 34 | return false; |
33 | } | 35 | } |
34 | if (value.must()) { | 36 | if (value.must()) { |
37 | if (targetRefiner == null) { | ||
38 | // Access the target refinery lazily, since it may be constructed after this refiner. | ||
39 | targetRefiner = getAdapter().getRefiner(targetType); | ||
40 | } | ||
35 | return sourceRefiner.merge(Tuple.of(key.get(0)), TruthValue.TRUE) && | 41 | return sourceRefiner.merge(Tuple.of(key.get(0)), TruthValue.TRUE) && |
36 | targetRefiner.merge(Tuple.of(key.get(1)), TruthValue.TRUE); | 42 | targetRefiner.merge(Tuple.of(key.get(1)), TruthValue.TRUE); |
37 | } | 43 | } |
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java index ad0288ed..74022fc6 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/translator/metamodel/MetamodelBuilder.java | |||
@@ -153,15 +153,11 @@ public class MetamodelBuilder { | |||
153 | return; | 153 | return; |
154 | } | 154 | } |
155 | var sourceType = info.sourceType(); | 155 | var sourceType = info.sourceType(); |
156 | var targetType = info.targetType(); | ||
157 | if (typeHierarchyBuilder.isInvalidType(sourceType)) { | 156 | if (typeHierarchyBuilder.isInvalidType(sourceType)) { |
158 | throw new TranslationException(linkType, "Source type %s of %s is not in type hierarchy" | 157 | throw new TranslationException(linkType, "Source type %s of %s is not in type hierarchy" |
159 | .formatted(sourceType, linkType)); | 158 | .formatted(sourceType, linkType)); |
160 | } | 159 | } |
161 | if (typeHierarchyBuilder.isInvalidType(targetType)) { | 160 | var targetType = info.targetType(); |
162 | throw new TranslationException(linkType, "Target type %s of %s is not in type hierarchy" | ||
163 | .formatted(targetType, linkType)); | ||
164 | } | ||
165 | var opposite = info.opposite(); | 161 | var opposite = info.opposite(); |
166 | Multiplicity targetMultiplicity = UnconstrainedMultiplicity.INSTANCE; | 162 | Multiplicity targetMultiplicity = UnconstrainedMultiplicity.INSTANCE; |
167 | if (opposite != null) { | 163 | if (opposite != null) { |
@@ -185,18 +181,30 @@ public class MetamodelBuilder { | |||
185 | oppositeReferences.put(opposite, linkType); | 181 | oppositeReferences.put(opposite, linkType); |
186 | } | 182 | } |
187 | if (info.containment()) { | 183 | if (info.containment()) { |
188 | if (!UnconstrainedMultiplicity.INSTANCE.equals(targetMultiplicity)) { | 184 | processContainmentInfo(linkType, info, targetMultiplicity); |
189 | throw new TranslationException(opposite, "Invalid opposite %s with multiplicity %s of containment %s" | ||
190 | .formatted(opposite, targetMultiplicity, linkType)); | ||
191 | } | ||
192 | containedTypes.add(targetType); | ||
193 | containmentHierarchy.put(linkType, new ContainmentInfo(sourceType, info.multiplicity(), targetType)); | ||
194 | return; | 185 | return; |
195 | } | 186 | } |
196 | directedCrossReferences.put(linkType, new DirectedCrossReferenceInfo(sourceType, info.multiplicity(), | 187 | directedCrossReferences.put(linkType, new DirectedCrossReferenceInfo(sourceType, info.multiplicity(), |
197 | targetType, targetMultiplicity)); | 188 | targetType, targetMultiplicity)); |
198 | } | 189 | } |
199 | 190 | ||
191 | private void processContainmentInfo(PartialRelation linkType, ReferenceInfo info, | ||
192 | Multiplicity targetMultiplicity) { | ||
193 | var sourceType = info.sourceType(); | ||
194 | var targetType = info.targetType(); | ||
195 | var opposite = info.opposite(); | ||
196 | if (typeHierarchyBuilder.isInvalidType(targetType)) { | ||
197 | throw new TranslationException(linkType, "Target type %s of %s is not in type hierarchy" | ||
198 | .formatted(targetType, linkType)); | ||
199 | } | ||
200 | if (!UnconstrainedMultiplicity.INSTANCE.equals(targetMultiplicity)) { | ||
201 | throw new TranslationException(opposite, "Invalid opposite %s with multiplicity %s of containment %s" | ||
202 | .formatted(opposite, targetMultiplicity, linkType)); | ||
203 | } | ||
204 | containedTypes.add(targetType); | ||
205 | containmentHierarchy.put(linkType, new ContainmentInfo(sourceType, info.multiplicity(), targetType)); | ||
206 | } | ||
207 | |||
200 | private static void validateOpposite(PartialRelation linkType, ReferenceInfo info, PartialRelation opposite, | 208 | private static void validateOpposite(PartialRelation linkType, ReferenceInfo info, PartialRelation opposite, |
201 | ReferenceInfo oppositeInfo) { | 209 | ReferenceInfo oppositeInfo) { |
202 | var sourceType = info.sourceType(); | 210 | var sourceType = info.sourceType(); |