aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2024-02-18 22:19:44 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2024-02-18 22:19:44 +0100
commitb9bb6816a8806fe4e918c8a2074364676737cc0c (patch)
tree4b9c822d4996854adc89ca24935a6964af0d5f7d
parentrefactor(language): no fully qualified self import (diff)
downloadrefinery-b9bb6816a8806fe4e918c8a2074364676737cc0c.tar.gz
refinery-b9bb6816a8806fe4e918c8a2074364676737cc0c.tar.zst
refinery-b9bb6816a8806fe4e918c8a2074364676737cc0c.zip
feat(language): import validation
Validate imports and imported resources. Also fixes a linking error in imported resources by ensuring that imported resources are always fully resolved with all of their derived state.
-rw-r--r--subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java99
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java3
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java2
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java69
4 files changed, 153 insertions, 20 deletions
diff --git a/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java b/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java
index e44dddc0..580a87b6 100644
--- a/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java
+++ b/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java
@@ -9,17 +9,24 @@ import com.google.inject.Inject;
9import com.google.inject.Provider; 9import com.google.inject.Provider;
10import org.eclipse.emf.common.util.URI; 10import org.eclipse.emf.common.util.URI;
11import org.eclipse.emf.ecore.resource.Resource; 11import org.eclipse.emf.ecore.resource.Resource;
12import org.eclipse.emf.ecore.util.EcoreUtil;
12import org.eclipse.xtext.diagnostics.Severity; 13import org.eclipse.xtext.diagnostics.Severity;
13import org.eclipse.xtext.resource.FileExtensionProvider; 14import org.eclipse.xtext.naming.IQualifiedNameConverter;
14import org.eclipse.xtext.resource.IResourceFactory; 15import org.eclipse.xtext.resource.*;
15import org.eclipse.xtext.resource.XtextResourceSet; 16import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider;
17import org.eclipse.xtext.util.CancelIndicator;
16import org.eclipse.xtext.util.LazyStringInputStream; 18import org.eclipse.xtext.util.LazyStringInputStream;
17import org.eclipse.xtext.validation.CheckMode; 19import org.eclipse.xtext.validation.CheckMode;
18import org.eclipse.xtext.validation.IResourceValidator; 20import org.eclipse.xtext.validation.IResourceValidator;
21import org.eclipse.xtext.validation.Issue;
19import tools.refinery.language.model.problem.Problem; 22import tools.refinery.language.model.problem.Problem;
20import tools.refinery.language.model.problem.Relation; 23import tools.refinery.language.model.problem.Relation;
21import tools.refinery.language.model.problem.ScopeDeclaration; 24import tools.refinery.language.model.problem.ScopeDeclaration;
25import tools.refinery.language.naming.NamingUtil;
26import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
27import tools.refinery.language.resource.ProblemResourceDescriptionStrategy.ShadowingKey;
22import tools.refinery.language.scoping.imports.ImportAdapter; 28import tools.refinery.language.scoping.imports.ImportAdapter;
29import tools.refinery.language.scoping.imports.ImportCollector;
23import tools.refinery.store.util.CancellationToken; 30import tools.refinery.store.util.CancellationToken;
24 31
25import java.io.ByteArrayOutputStream; 32import java.io.ByteArrayOutputStream;
@@ -28,10 +35,8 @@ import java.io.IOException;
28import java.io.InputStream; 35import java.io.InputStream;
29import java.nio.charset.StandardCharsets; 36import java.nio.charset.StandardCharsets;
30import java.nio.file.Path; 37import java.nio.file.Path;
31import java.util.ArrayList; 38import java.util.*;
32import java.util.HashSet; 39import java.util.stream.Collectors;
33import java.util.List;
34import java.util.Map;
35 40
36// This class is used as a fluent builder. 41// This class is used as a fluent builder.
37@SuppressWarnings("UnusedReturnValue") 42@SuppressWarnings("UnusedReturnValue")
@@ -47,6 +52,15 @@ public class ProblemLoader {
47 @Inject 52 @Inject
48 private IResourceValidator resourceValidator; 53 private IResourceValidator resourceValidator;
49 54
55 @Inject
56 private ImportCollector importCollector;
57
58 @Inject
59 private GlobalResourceDescriptionProvider globalResourceDescriptionProvider;
60
61 @Inject
62 private IQualifiedNameConverter qualifiedNameConverter;
63
50 private CancellationToken cancellationToken = CancellationToken.NONE; 64 private CancellationToken cancellationToken = CancellationToken.NONE;
51 65
52 private final List<Path> extraPaths = new ArrayList<>(); 66 private final List<Path> extraPaths = new ArrayList<>();
@@ -117,14 +131,30 @@ public class ProblemLoader {
117 } 131 }
118 132
119 public Problem loadResource(Resource resource) { 133 public Problem loadResource(Resource resource) {
120 var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> { 134 EcoreUtil.resolveAll(resource);
135 CancelIndicator cancelIndicator = () -> {
121 cancellationToken.checkCancelled(); 136 cancellationToken.checkCancelled();
122 return Thread.interrupted(); 137 return Thread.interrupted();
123 }); 138 };
139 var shadowedNames = new LinkedHashMap<ShadowingKey, Set<IEObjectDescription>>();
140 var issues = new ArrayList<Issue>();
141 validateResource(resource, issues, cancelIndicator);
124 cancellationToken.checkCancelled(); 142 cancellationToken.checkCancelled();
125 var errors = issues.stream() 143 var resourceSet = resource.getResourceSet();
126 .filter(issue -> issue.getSeverity() == Severity.ERROR) 144 if (resourceSet != null) {
127 .toList(); 145 var imports = importCollector.getAllImports(resource).toUriSet();
146 cancellationToken.checkCancelled();
147 for (var importedUri : imports) {
148 var importedResource = resourceSet.getResource(importedUri, false);
149 if (importedResource == null) {
150 throw new IllegalStateException("Unknown imported resource: " + importedUri);
151 }
152 findShadowedNames(importedResource, shadowedNames);
153 validateResource(importedResource, issues, cancelIndicator);
154 }
155 }
156 addNameClashIssues(issues, shadowedNames);
157 var errors = issues.stream().filter(issue -> issue.getSeverity() == Severity.ERROR).toList();
128 if (!errors.isEmpty()) { 158 if (!errors.isEmpty()) {
129 throw new ValidationErrorsException(resource.getURI(), errors); 159 throw new ValidationErrorsException(resource.getURI(), errors);
130 } 160 }
@@ -134,8 +164,49 @@ public class ProblemLoader {
134 return problem; 164 return problem;
135 } 165 }
136 166
137 public Problem loadScopeConstraints(Problem problem, List<String> extraScopes, 167 private void findShadowedNames(Resource importedResource,
138 List<String> overrideScopes) throws IOException { 168 LinkedHashMap<ShadowingKey, Set<IEObjectDescription>> shadowedNames) {
169 var resourceDescription = globalResourceDescriptionProvider.getResourceDescription(importedResource);
170 for (var eObjectDescription : resourceDescription.getExportedObjects()) {
171 var name = eObjectDescription.getName();
172 if (NamingUtil.isFullyQualified(name)) {
173 var shadowingKey = ProblemResourceDescriptionStrategy.getShadowingKey(eObjectDescription);
174 var entries = shadowedNames.computeIfAbsent(shadowingKey, ignored -> new LinkedHashSet<>());
175 entries.add(eObjectDescription);
176 }
177 }
178 cancellationToken.checkCancelled();
179 }
180
181 private void validateResource(Resource importedResource, ArrayList<Issue> issues,
182 CancelIndicator cancelIndicator) {
183 issues.addAll(resourceValidator.validate(importedResource, CheckMode.ALL, cancelIndicator));
184 cancellationToken.checkCancelled();
185 }
186
187 private void addNameClashIssues(ArrayList<Issue> issues,
188 LinkedHashMap<ShadowingKey, Set<IEObjectDescription>> shadowedNames) {
189 for (var entry : shadowedNames.entrySet()) {
190 var eObjectDescriptions = entry.getValue();
191 if (eObjectDescriptions.size() <= 1) {
192 continue;
193 }
194 var qualifiedName = qualifiedNameConverter.toString(NamingUtil.stripRootPrefix(entry.getKey().name()));
195 var uris = eObjectDescriptions.stream()
196 .map(eObjectDescription -> eObjectDescription.getEObjectURI().trimFragment().toString())
197 .collect(Collectors.joining(", "));
198 var message = "Object with qualified name %s is also defined in %s".formatted(qualifiedName, uris);
199 for (var eObjectDescription : eObjectDescriptions) {
200 var issue = new Issue.IssueImpl();
201 issue.setSeverity(Severity.ERROR);
202 issue.setMessage(message);
203 issue.setUriToProblem(eObjectDescription.getEObjectURI());
204 issues.add(issue);
205 }
206 }
207 }
208
209 public Problem loadScopeConstraints(Problem problem, List<String> extraScopes, List<String> overrideScopes) throws IOException {
139 var allScopes = new ArrayList<>(extraScopes); 210 var allScopes = new ArrayList<>(extraScopes);
140 allScopes.addAll(overrideScopes); 211 allScopes.addAll(overrideScopes);
141 if (allScopes.isEmpty()) { 212 if (allScopes.isEmpty()) {
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java b/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java
index 373a32f2..ccadb42d 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/resource/LoadOnDemandResourceDescriptionProvider.java
@@ -8,6 +8,7 @@ package tools.refinery.language.resource;
8import com.google.inject.Inject; 8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.URI; 9import org.eclipse.emf.common.util.URI;
10import org.eclipse.emf.ecore.resource.Resource; 10import org.eclipse.emf.ecore.resource.Resource;
11import org.eclipse.emf.ecore.util.EcoreUtil;
11import org.eclipse.xtext.EcoreUtil2; 12import org.eclipse.xtext.EcoreUtil2;
12import org.eclipse.xtext.resource.IResourceDescription; 13import org.eclipse.xtext.resource.IResourceDescription;
13import org.eclipse.xtext.resource.IResourceDescriptions; 14import org.eclipse.xtext.resource.IResourceDescriptions;
@@ -44,6 +45,8 @@ public class LoadOnDemandResourceDescriptionProvider {
44 if (importedResource == null) { 45 if (importedResource == null) {
45 return null; 46 return null;
46 } 47 }
48 // Force the {@code importedResource} to have all of its derived resource state installed.
49 EcoreUtil.resolveAll(importedResource);
47 return globalResourceDescriptionProvider.getResourceDescription(importedResource); 50 return globalResourceDescriptionProvider.getResourceDescription(importedResource);
48 } 51 }
49} 52}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java
index fc4ca43c..ac5a92ba 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportCollector.java
@@ -150,7 +150,7 @@ public class ImportCollector {
150 150
151 protected List<URI> getImports(IEObjectDescription eObjectDescription) { 151 protected List<URI> getImports(IEObjectDescription eObjectDescription) {
152 var importString = eObjectDescription.getUserData(ProblemResourceDescriptionStrategy.IMPORTS); 152 var importString = eObjectDescription.getUserData(ProblemResourceDescriptionStrategy.IMPORTS);
153 if (importString == null) { 153 if (importString == null || importString.isEmpty()) {
154 return List.of(); 154 return List.of();
155 } 155 }
156 return Splitter.on(ProblemResourceDescriptionStrategy.IMPORTS_SEPARATOR).splitToStream(importString) 156 return Splitter.on(ProblemResourceDescriptionStrategy.IMPORTS_SEPARATOR).splitToStream(importString)
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 4cbb02c2..d9eb5fd3 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
@@ -1,5 +1,5 @@
1/* 1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> 2 * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/>
3 * 3 *
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
@@ -13,16 +13,16 @@ import com.google.inject.Inject;
13import org.eclipse.emf.ecore.EObject; 13import org.eclipse.emf.ecore.EObject;
14import org.eclipse.emf.ecore.EReference; 14import org.eclipse.emf.ecore.EReference;
15import org.eclipse.xtext.EcoreUtil2; 15import org.eclipse.xtext.EcoreUtil2;
16import org.eclipse.xtext.naming.IQualifiedNameConverter;
16import org.eclipse.xtext.validation.Check; 17import org.eclipse.xtext.validation.Check;
17import org.jetbrains.annotations.Nullable; 18import org.jetbrains.annotations.Nullable;
18import tools.refinery.language.model.problem.*; 19import tools.refinery.language.model.problem.*;
20import tools.refinery.language.naming.NamingUtil;
21import tools.refinery.language.scoping.imports.ImportAdapter;
19import tools.refinery.language.utils.ProblemDesugarer; 22import tools.refinery.language.utils.ProblemDesugarer;
20import tools.refinery.language.utils.ProblemUtil; 23import tools.refinery.language.utils.ProblemUtil;
21 24
22import java.util.ArrayList; 25import java.util.*;
23import java.util.LinkedHashMap;
24import java.util.LinkedHashSet;
25import java.util.Set;
26 26
27/** 27/**
28 * This class contains custom validation rules. 28 * This class contains custom validation rules.
@@ -33,6 +33,10 @@ import java.util.Set;
33public class ProblemValidator extends AbstractProblemValidator { 33public class ProblemValidator extends AbstractProblemValidator {
34 private static final String ISSUE_PREFIX = "tools.refinery.language.validation.ProblemValidator."; 34 private static final String ISSUE_PREFIX = "tools.refinery.language.validation.ProblemValidator.";
35 35
36 public static final String UNEXPECTED_MODULE_NAME_ISSUE = ISSUE_PREFIX + "UNEXPECTED_MODULE_NAME";
37
38 public static final String INVALID_IMPORT_ISSUE = ISSUE_PREFIX + "INVALID_IMPORT";
39
36 public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; 40 public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE";
37 41
38 public static final String NODE_CONSTANT_ISSUE = ISSUE_PREFIX + "NODE_CONSTANT_ISSUE"; 42 public static final String NODE_CONSTANT_ISSUE = ISSUE_PREFIX + "NODE_CONSTANT_ISSUE";
@@ -65,6 +69,61 @@ public class ProblemValidator extends AbstractProblemValidator {
65 @Inject 69 @Inject
66 private ProblemDesugarer desugarer; 70 private ProblemDesugarer desugarer;
67 71
72 @Inject
73 private IQualifiedNameConverter qualifiedNameConverter;
74
75 @Check
76 public void checkModuleName(Problem problem) {
77 var nameString = problem.getName();
78 if (nameString == null) {
79 return;
80 }
81 var resource = problem.eResource();
82 if (resource == null) {
83 return;
84 }
85 var resourceSet = resource.getResourceSet();
86 if (resourceSet == null) {
87 return;
88 }
89 var adapter = ImportAdapter.getOrInstall(resourceSet);
90 var expectedName = adapter.getQualifiedName(resource.getURI());
91 if (expectedName == null) {
92 return;
93 }
94 var name = NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(nameString));
95 if (!expectedName.equals(name)) {
96 var moduleKindName = switch (problem.getKind()) {
97 case PROBLEM -> "problem";
98 case MODULE -> "module";
99 };
100 var message = "Expected %s to have name '%s', got '%s' instead.".formatted(
101 moduleKindName, qualifiedNameConverter.toString(expectedName),
102 qualifiedNameConverter.toString(name));
103 error(message, problem, ProblemPackage.Literals.NAMED_ELEMENT__NAME, INSIGNIFICANT_INDEX,
104 UNEXPECTED_MODULE_NAME_ISSUE);
105 }
106 }
107
108 @Check
109 public void checkImportStatement(ImportStatement importStatement) {
110 var importedModule = importStatement.getImportedModule();
111 if (importedModule == null || importedModule.eIsProxy()) {
112 return;
113 }
114 String message = null;
115 var problem = EcoreUtil2.getContainerOfType(importStatement, Problem.class);
116 if (importedModule == problem) {
117 message = "A module cannot import itself.";
118 } else if (importedModule.getKind() != ModuleKind.MODULE) {
119 message = "Only modules can be imported.";
120 }
121 if (message != null) {
122 error(message, importStatement, ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE,
123 INSIGNIFICANT_INDEX, INVALID_IMPORT_ISSUE);
124 }
125 }
126
68 @Check 127 @Check
69 public void checkSingletonVariable(VariableOrNodeExpr expr) { 128 public void checkSingletonVariable(VariableOrNodeExpr expr) {
70 var variableOrNode = expr.getVariableOrNode(); 129 var variableOrNode = expr.getVariableOrNode();