From b9bb6816a8806fe4e918c8a2074364676737cc0c Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 18 Feb 2024 22:19:44 +0100 Subject: 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. --- .../tools/refinery/generator/ProblemLoader.java | 99 +++++++++++++++++++--- .../LoadOnDemandResourceDescriptionProvider.java | 3 + .../language/scoping/imports/ImportCollector.java | 2 +- .../language/validation/ProblemValidator.java | 69 +++++++++++++-- 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; import com.google.inject.Provider; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.xtext.diagnostics.Severity; -import org.eclipse.xtext.resource.FileExtensionProvider; -import org.eclipse.xtext.resource.IResourceFactory; -import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.resource.*; +import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider; +import org.eclipse.xtext.util.CancelIndicator; import org.eclipse.xtext.util.LazyStringInputStream; import org.eclipse.xtext.validation.CheckMode; import org.eclipse.xtext.validation.IResourceValidator; +import org.eclipse.xtext.validation.Issue; import tools.refinery.language.model.problem.Problem; import tools.refinery.language.model.problem.Relation; import tools.refinery.language.model.problem.ScopeDeclaration; +import tools.refinery.language.naming.NamingUtil; +import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; +import tools.refinery.language.resource.ProblemResourceDescriptionStrategy.ShadowingKey; import tools.refinery.language.scoping.imports.ImportAdapter; +import tools.refinery.language.scoping.imports.ImportCollector; import tools.refinery.store.util.CancellationToken; import java.io.ByteArrayOutputStream; @@ -28,10 +35,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; // This class is used as a fluent builder. @SuppressWarnings("UnusedReturnValue") @@ -47,6 +52,15 @@ public class ProblemLoader { @Inject private IResourceValidator resourceValidator; + @Inject + private ImportCollector importCollector; + + @Inject + private GlobalResourceDescriptionProvider globalResourceDescriptionProvider; + + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + private CancellationToken cancellationToken = CancellationToken.NONE; private final List extraPaths = new ArrayList<>(); @@ -117,14 +131,30 @@ public class ProblemLoader { } public Problem loadResource(Resource resource) { - var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> { + EcoreUtil.resolveAll(resource); + CancelIndicator cancelIndicator = () -> { cancellationToken.checkCancelled(); return Thread.interrupted(); - }); + }; + var shadowedNames = new LinkedHashMap>(); + var issues = new ArrayList(); + validateResource(resource, issues, cancelIndicator); cancellationToken.checkCancelled(); - var errors = issues.stream() - .filter(issue -> issue.getSeverity() == Severity.ERROR) - .toList(); + var resourceSet = resource.getResourceSet(); + if (resourceSet != null) { + var imports = importCollector.getAllImports(resource).toUriSet(); + cancellationToken.checkCancelled(); + for (var importedUri : imports) { + var importedResource = resourceSet.getResource(importedUri, false); + if (importedResource == null) { + throw new IllegalStateException("Unknown imported resource: " + importedUri); + } + findShadowedNames(importedResource, shadowedNames); + validateResource(importedResource, issues, cancelIndicator); + } + } + addNameClashIssues(issues, shadowedNames); + var errors = issues.stream().filter(issue -> issue.getSeverity() == Severity.ERROR).toList(); if (!errors.isEmpty()) { throw new ValidationErrorsException(resource.getURI(), errors); } @@ -134,8 +164,49 @@ public class ProblemLoader { return problem; } - public Problem loadScopeConstraints(Problem problem, List extraScopes, - List overrideScopes) throws IOException { + private void findShadowedNames(Resource importedResource, + LinkedHashMap> shadowedNames) { + var resourceDescription = globalResourceDescriptionProvider.getResourceDescription(importedResource); + for (var eObjectDescription : resourceDescription.getExportedObjects()) { + var name = eObjectDescription.getName(); + if (NamingUtil.isFullyQualified(name)) { + var shadowingKey = ProblemResourceDescriptionStrategy.getShadowingKey(eObjectDescription); + var entries = shadowedNames.computeIfAbsent(shadowingKey, ignored -> new LinkedHashSet<>()); + entries.add(eObjectDescription); + } + } + cancellationToken.checkCancelled(); + } + + private void validateResource(Resource importedResource, ArrayList issues, + CancelIndicator cancelIndicator) { + issues.addAll(resourceValidator.validate(importedResource, CheckMode.ALL, cancelIndicator)); + cancellationToken.checkCancelled(); + } + + private void addNameClashIssues(ArrayList issues, + LinkedHashMap> shadowedNames) { + for (var entry : shadowedNames.entrySet()) { + var eObjectDescriptions = entry.getValue(); + if (eObjectDescriptions.size() <= 1) { + continue; + } + var qualifiedName = qualifiedNameConverter.toString(NamingUtil.stripRootPrefix(entry.getKey().name())); + var uris = eObjectDescriptions.stream() + .map(eObjectDescription -> eObjectDescription.getEObjectURI().trimFragment().toString()) + .collect(Collectors.joining(", ")); + var message = "Object with qualified name %s is also defined in %s".formatted(qualifiedName, uris); + for (var eObjectDescription : eObjectDescriptions) { + var issue = new Issue.IssueImpl(); + issue.setSeverity(Severity.ERROR); + issue.setMessage(message); + issue.setUriToProblem(eObjectDescription.getEObjectURI()); + issues.add(issue); + } + } + } + + public Problem loadScopeConstraints(Problem problem, List extraScopes, List overrideScopes) throws IOException { var allScopes = new ArrayList<>(extraScopes); allScopes.addAll(overrideScopes); 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; import com.google.inject.Inject; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.resource.IResourceDescription; import org.eclipse.xtext.resource.IResourceDescriptions; @@ -44,6 +45,8 @@ public class LoadOnDemandResourceDescriptionProvider { if (importedResource == null) { return null; } + // Force the {@code importedResource} to have all of its derived resource state installed. + EcoreUtil.resolveAll(importedResource); return globalResourceDescriptionProvider.getResourceDescription(importedResource); } } 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 { protected List getImports(IEObjectDescription eObjectDescription) { var importString = eObjectDescription.getUserData(ProblemResourceDescriptionStrategy.IMPORTS); - if (importString == null) { + if (importString == null || importString.isEmpty()) { return List.of(); } 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 @@ /* - * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ @@ -13,16 +13,16 @@ import com.google.inject.Inject; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.naming.IQualifiedNameConverter; import org.eclipse.xtext.validation.Check; import org.jetbrains.annotations.Nullable; import tools.refinery.language.model.problem.*; +import tools.refinery.language.naming.NamingUtil; +import tools.refinery.language.scoping.imports.ImportAdapter; import tools.refinery.language.utils.ProblemDesugarer; import tools.refinery.language.utils.ProblemUtil; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; /** * This class contains custom validation rules. @@ -33,6 +33,10 @@ import java.util.Set; public class ProblemValidator extends AbstractProblemValidator { private static final String ISSUE_PREFIX = "tools.refinery.language.validation.ProblemValidator."; + public static final String UNEXPECTED_MODULE_NAME_ISSUE = ISSUE_PREFIX + "UNEXPECTED_MODULE_NAME"; + + public static final String INVALID_IMPORT_ISSUE = ISSUE_PREFIX + "INVALID_IMPORT"; + public static final String SINGLETON_VARIABLE_ISSUE = ISSUE_PREFIX + "SINGLETON_VARIABLE"; public static final String NODE_CONSTANT_ISSUE = ISSUE_PREFIX + "NODE_CONSTANT_ISSUE"; @@ -65,6 +69,61 @@ public class ProblemValidator extends AbstractProblemValidator { @Inject private ProblemDesugarer desugarer; + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + + @Check + public void checkModuleName(Problem problem) { + var nameString = problem.getName(); + if (nameString == null) { + return; + } + var resource = problem.eResource(); + if (resource == null) { + return; + } + var resourceSet = resource.getResourceSet(); + if (resourceSet == null) { + return; + } + var adapter = ImportAdapter.getOrInstall(resourceSet); + var expectedName = adapter.getQualifiedName(resource.getURI()); + if (expectedName == null) { + return; + } + var name = NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(nameString)); + if (!expectedName.equals(name)) { + var moduleKindName = switch (problem.getKind()) { + case PROBLEM -> "problem"; + case MODULE -> "module"; + }; + var message = "Expected %s to have name '%s', got '%s' instead.".formatted( + moduleKindName, qualifiedNameConverter.toString(expectedName), + qualifiedNameConverter.toString(name)); + error(message, problem, ProblemPackage.Literals.NAMED_ELEMENT__NAME, INSIGNIFICANT_INDEX, + UNEXPECTED_MODULE_NAME_ISSUE); + } + } + + @Check + public void checkImportStatement(ImportStatement importStatement) { + var importedModule = importStatement.getImportedModule(); + if (importedModule == null || importedModule.eIsProxy()) { + return; + } + String message = null; + var problem = EcoreUtil2.getContainerOfType(importStatement, Problem.class); + if (importedModule == problem) { + message = "A module cannot import itself."; + } else if (importedModule.getKind() != ModuleKind.MODULE) { + message = "Only modules can be imported."; + } + if (message != null) { + error(message, importStatement, ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE, + INSIGNIFICANT_INDEX, INVALID_IMPORT_ISSUE); + } + } + @Check public void checkSingletonVariable(VariableOrNodeExpr expr) { var variableOrNode = expr.getVariableOrNode(); -- cgit v1.2.3-70-g09d2