diff options
author | Kristóf Marussy <kristof@marussy.com> | 2024-02-18 22:19:44 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2024-02-18 22:19:44 +0100 |
commit | b9bb6816a8806fe4e918c8a2074364676737cc0c (patch) | |
tree | 4b9c822d4996854adc89ca24935a6964af0d5f7d /subprojects | |
parent | refactor(language): no fully qualified self import (diff) | |
download | refinery-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.
Diffstat (limited to 'subprojects')
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; | |||
9 | import com.google.inject.Provider; | 9 | import com.google.inject.Provider; |
10 | import org.eclipse.emf.common.util.URI; | 10 | import org.eclipse.emf.common.util.URI; |
11 | import org.eclipse.emf.ecore.resource.Resource; | 11 | import org.eclipse.emf.ecore.resource.Resource; |
12 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
12 | import org.eclipse.xtext.diagnostics.Severity; | 13 | import org.eclipse.xtext.diagnostics.Severity; |
13 | import org.eclipse.xtext.resource.FileExtensionProvider; | 14 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
14 | import org.eclipse.xtext.resource.IResourceFactory; | 15 | import org.eclipse.xtext.resource.*; |
15 | import org.eclipse.xtext.resource.XtextResourceSet; | 16 | import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider; |
17 | import org.eclipse.xtext.util.CancelIndicator; | ||
16 | import org.eclipse.xtext.util.LazyStringInputStream; | 18 | import org.eclipse.xtext.util.LazyStringInputStream; |
17 | import org.eclipse.xtext.validation.CheckMode; | 19 | import org.eclipse.xtext.validation.CheckMode; |
18 | import org.eclipse.xtext.validation.IResourceValidator; | 20 | import org.eclipse.xtext.validation.IResourceValidator; |
21 | import org.eclipse.xtext.validation.Issue; | ||
19 | import tools.refinery.language.model.problem.Problem; | 22 | import tools.refinery.language.model.problem.Problem; |
20 | import tools.refinery.language.model.problem.Relation; | 23 | import tools.refinery.language.model.problem.Relation; |
21 | import tools.refinery.language.model.problem.ScopeDeclaration; | 24 | import tools.refinery.language.model.problem.ScopeDeclaration; |
25 | import tools.refinery.language.naming.NamingUtil; | ||
26 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | ||
27 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy.ShadowingKey; | ||
22 | import tools.refinery.language.scoping.imports.ImportAdapter; | 28 | import tools.refinery.language.scoping.imports.ImportAdapter; |
29 | import tools.refinery.language.scoping.imports.ImportCollector; | ||
23 | import tools.refinery.store.util.CancellationToken; | 30 | import tools.refinery.store.util.CancellationToken; |
24 | 31 | ||
25 | import java.io.ByteArrayOutputStream; | 32 | import java.io.ByteArrayOutputStream; |
@@ -28,10 +35,8 @@ import java.io.IOException; | |||
28 | import java.io.InputStream; | 35 | import java.io.InputStream; |
29 | import java.nio.charset.StandardCharsets; | 36 | import java.nio.charset.StandardCharsets; |
30 | import java.nio.file.Path; | 37 | import java.nio.file.Path; |
31 | import java.util.ArrayList; | 38 | import java.util.*; |
32 | import java.util.HashSet; | 39 | import java.util.stream.Collectors; |
33 | import java.util.List; | ||
34 | import 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; | |||
8 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
9 | import org.eclipse.emf.common.util.URI; | 9 | import org.eclipse.emf.common.util.URI; |
10 | import org.eclipse.emf.ecore.resource.Resource; | 10 | import org.eclipse.emf.ecore.resource.Resource; |
11 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
11 | import org.eclipse.xtext.EcoreUtil2; | 12 | import org.eclipse.xtext.EcoreUtil2; |
12 | import org.eclipse.xtext.resource.IResourceDescription; | 13 | import org.eclipse.xtext.resource.IResourceDescription; |
13 | import org.eclipse.xtext.resource.IResourceDescriptions; | 14 | import 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; | |||
13 | import org.eclipse.emf.ecore.EObject; | 13 | import org.eclipse.emf.ecore.EObject; |
14 | import org.eclipse.emf.ecore.EReference; | 14 | import org.eclipse.emf.ecore.EReference; |
15 | import org.eclipse.xtext.EcoreUtil2; | 15 | import org.eclipse.xtext.EcoreUtil2; |
16 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
16 | import org.eclipse.xtext.validation.Check; | 17 | import org.eclipse.xtext.validation.Check; |
17 | import org.jetbrains.annotations.Nullable; | 18 | import org.jetbrains.annotations.Nullable; |
18 | import tools.refinery.language.model.problem.*; | 19 | import tools.refinery.language.model.problem.*; |
20 | import tools.refinery.language.naming.NamingUtil; | ||
21 | import tools.refinery.language.scoping.imports.ImportAdapter; | ||
19 | import tools.refinery.language.utils.ProblemDesugarer; | 22 | import tools.refinery.language.utils.ProblemDesugarer; |
20 | import tools.refinery.language.utils.ProblemUtil; | 23 | import tools.refinery.language.utils.ProblemUtil; |
21 | 24 | ||
22 | import java.util.ArrayList; | 25 | import java.util.*; |
23 | import java.util.LinkedHashMap; | ||
24 | import java.util.LinkedHashSet; | ||
25 | import 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; | |||
33 | public class ProblemValidator extends AbstractProblemValidator { | 33 | public 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(); |