From 4dd2b4e1f45bf940d969181ae9b9176fa353c92e Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 4 Feb 2024 22:06:56 +0100 Subject: feat: filesystem-level import resolution Modules without an explicitly declared name get a name automatically inferred from their path. --- .../refinery/language/library/BuiltinLibrary.java | 29 +-- .../language/library/ClasspathBasedLibrary.java | 98 +++++++++ .../refinery/language/library/PathLibrary.java | 90 ++++++++ .../language/library/RefineryLibraries.java | 54 ----- .../refinery/language/library/RefineryLibrary.java | 7 +- .../ProblemDelegateQualifiedNameProvider.java | 34 ++- .../ProblemResourceDescriptionStrategy.java | 27 ++- .../scoping/ProblemLocalScopeProvider.java | 8 +- .../language/scoping/imports/ImportAdapter.java | 230 +++++++++++++++++++++ .../language/scoping/imports/ImportCollector.java | 42 +++- .../tools/refinery/language/utils/ProblemUtil.java | 6 +- ...tools.refinery.language.library.RefineryLibrary | 1 + .../refinery/language/library/builtin.refinery | 4 +- 13 files changed, 524 insertions(+), 106 deletions(-) create mode 100644 subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java delete mode 100644 subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java create mode 100644 subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java (limited to 'subprojects/language/src') diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java index 0fb4cd2c..d88d0299 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java +++ b/subprojects/language/src/main/java/tools/refinery/language/library/BuiltinLibrary.java @@ -9,33 +9,14 @@ import org.eclipse.emf.common.util.URI; import org.eclipse.xtext.naming.QualifiedName; import java.util.List; -import java.util.Optional; -public class BuiltinLibrary implements RefineryLibrary { +public class BuiltinLibrary extends ClasspathBasedLibrary { public static final QualifiedName BUILTIN_LIBRARY_NAME = QualifiedName.create("builtin"); - public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME).orElseThrow( + public static final URI BUILTIN_LIBRARY_URI = ClasspathBasedLibrary.getLibraryUri( + BuiltinLibrary.class, BUILTIN_LIBRARY_NAME).orElseThrow( () -> new IllegalStateException("Builtin library was not found")); - @Override - public List getAutomaticImports() { - return List.of(BUILTIN_LIBRARY_NAME); - } - - @Override - public Optional resolveQualifiedName(QualifiedName qualifiedName) { - if (qualifiedName.startsWith(BUILTIN_LIBRARY_NAME)) { - return getLibraryUri(qualifiedName); - } - return Optional.empty(); - } - - private static Optional getLibraryUri(QualifiedName qualifiedName) { - var libraryPath = String.join("/", qualifiedName.getSegments()); - var libraryResource = BuiltinLibrary.class.getClassLoader() - .getResource("tools/refinery/language/library/%s.refinery".formatted(libraryPath)); - if (libraryResource == null) { - return Optional.empty(); - } - return Optional.of(URI.createURI(libraryResource.toString())); + public BuiltinLibrary() { + super(BUILTIN_LIBRARY_NAME, List.of(BUILTIN_LIBRARY_NAME)); } } diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java new file mode 100644 index 00000000..4b748c64 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/ClasspathBasedLibrary.java @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.library; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; + +import java.nio.file.Path; +import java.util.*; + +public abstract class ClasspathBasedLibrary implements RefineryLibrary { + private final QualifiedName prefix; + private final List automaticImports; + private final URI rootUri; + + protected ClasspathBasedLibrary(QualifiedName prefix, List automaticImports) { + this.prefix = prefix; + this.automaticImports = List.copyOf(automaticImports); + var context = this.getClass(); + var contextPath = context.getCanonicalName().replace('.', '/') + ".class"; + var contextResource = context.getClassLoader().getResource(contextPath); + if (contextResource == null) { + throw new IllegalStateException("Failed to find library context"); + } + var contextUri = URI.createURI(contextResource.toString()); + var segments = Arrays.copyOf(contextUri.segments(), contextUri.segmentCount() - 1); + rootUri = URI.createHierarchicalURI(contextUri.scheme(), contextUri.authority(), contextUri.device(), + segments, null, null); + } + + protected ClasspathBasedLibrary(QualifiedName prefix) { + this(prefix, List.of()); + } + + @Override + public List getAutomaticImports() { + return automaticImports; + } + + @Override + public Optional resolveQualifiedName(QualifiedName qualifiedName, List libraryPaths) { + if (qualifiedName.startsWith(prefix)) { + return getLibraryUri(this.getClass(), qualifiedName); + } + return Optional.empty(); + } + + @Override + public Optional getQualifiedName(URI uri, List libraryPaths) { + if (!uri.isHierarchical() || + !Objects.equals(rootUri.scheme(), uri.scheme()) || + !Objects.equals(rootUri.authority(), uri.authority()) || + !Objects.equals(rootUri.device(), uri.device()) || + rootUri.segmentCount() >= uri.segmentCount()) { + return Optional.empty(); + } + int rootSegmentCount = rootUri.segmentCount(); + int uriSegmentCount = uri.segmentCount(); + if (!uri.segment(uriSegmentCount - 1).endsWith(RefineryLibrary.EXTENSION)) { + return Optional.empty(); + } + var segments = new ArrayList(); + int i = 0; + while (i < rootSegmentCount) { + if (!rootUri.segment(i).equals(uri.segment(i))) { + return Optional.empty(); + } + i++; + } + while (i < uriSegmentCount) { + var segment = uri.segment(i); + if (i == uriSegmentCount - 1) { + segment = segment.substring(0, segment.length() - RefineryLibrary.EXTENSION.length()); + } + segments.add(segment); + i++; + } + var qualifiedName = QualifiedName.create(segments); + if (!qualifiedName.startsWith(prefix)) { + return Optional.empty(); + } + return Optional.of(qualifiedName); + } + + public static Optional getLibraryUri(Class context, QualifiedName qualifiedName) { + var packagePath = context.getPackageName().replace('.', '/'); + var libraryPath = String.join("/", qualifiedName.getSegments()); + var resourceName = "%s/%s%s".formatted(packagePath, libraryPath, RefineryLibrary.EXTENSION); + var resource = context.getClassLoader().getResource(resourceName); + if (resource == null) { + return Optional.empty(); + } + return Optional.of(URI.createURI(resource.toString())); + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java new file mode 100644 index 00000000..c6f994df --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/library/PathLibrary.java @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.library; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.xtext.naming.QualifiedName; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public final class PathLibrary implements RefineryLibrary { + @Override + public Optional resolveQualifiedName(QualifiedName qualifiedName, List libraryPaths) { + if (libraryPaths.isEmpty()) { + return Optional.empty(); + } + if (qualifiedName.getSegmentCount() == 0) { + return Optional.empty(); + } + var relativePath = qualifiedNameToRelativePath(qualifiedName); + for (var library : libraryPaths) { + var absoluteResolvedPath = library.resolve(relativePath).toAbsolutePath().normalize(); + if (absoluteResolvedPath.startsWith(library) && Files.exists(absoluteResolvedPath)) { + var uri = URI.createFileURI(absoluteResolvedPath.toString()); + return Optional.of(uri); + } + } + return Optional.empty(); + } + + private static Path qualifiedNameToRelativePath(QualifiedName qualifiedName) { + int segmentCount = qualifiedName.getSegmentCount(); + String first = null; + var rest = new String[segmentCount - 1]; + for (var i = 0; i < segmentCount; i++) { + var segment = qualifiedName.getSegment(i); + if (i == segmentCount - 1) { + segment = segment + RefineryLibrary.EXTENSION; + } + if (i == 0) { + first = segment; + } else { + rest[i - 1] = segment; + } + } + if (first == null) { + throw new AssertionError("Expected qualified name to have non-null segments"); + } + return Path.of(first, rest); + } + + @Override + public Optional getQualifiedName(URI uri, List libraryPaths) { + if (libraryPaths.isEmpty()) { + return Optional.empty(); + } + if (!uri.isFile() || !uri.hasAbsolutePath()) { + return Optional.empty(); + } + var path = Path.of(uri.toFileString()); + for (var library : libraryPaths) { + if (path.startsWith(library)) { + return getRelativeQualifiedName(library, path); + } + } + return Optional.empty(); + } + + private static Optional getRelativeQualifiedName(Path library, Path path) { + var relativePath = path.relativize(library); + var segments = new ArrayList(); + for (Path value : relativePath) { + segments.add(value.toString()); + } + int lastIndex = segments.size() - 1; + var lastSegment = segments.get(lastIndex); + if (!lastSegment.endsWith(EXTENSION)) { + return Optional.empty(); + } + lastSegment = lastSegment.substring(0, lastSegment.length() - RefineryLibrary.EXTENSION.length()); + segments.set(lastIndex, lastSegment); + return Optional.of(QualifiedName.create(segments)); + } +} diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java deleted file mode 100644 index 0efca199..00000000 --- a/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibraries.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 The Refinery Authors - * - * SPDX-License-Identifier: EPL-2.0 - */ -package tools.refinery.language.library; - -import org.eclipse.emf.common.util.URI; -import org.eclipse.xtext.naming.QualifiedName; -import tools.refinery.language.scoping.imports.NamedImport; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; - -public final class RefineryLibraries { - private static final ServiceLoader SERVICE_LOADER = ServiceLoader.load(RefineryLibrary.class); - private static final List AUTOMATIC_IMPORTS; - - static { - var imports = new LinkedHashMap(); - for (var service : SERVICE_LOADER) { - for (var qualifiedName : service.getAutomaticImports()) { - var uri = service.resolveQualifiedName(qualifiedName).orElseThrow( - () -> new IllegalStateException("Automatic import %s was not found".formatted(qualifiedName))); - if (imports.put(qualifiedName, uri) != null) { - throw new IllegalStateException("Duplicate automatic import " + qualifiedName); - } - } - } - AUTOMATIC_IMPORTS = imports.entrySet().stream() - .map(entry -> NamedImport.implicit(entry.getValue(), entry.getKey())) - .toList(); - } - - private RefineryLibraries() { - throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); - } - - public static List getAutomaticImports() { - return AUTOMATIC_IMPORTS; - } - - public static Optional resolveQualifiedName(QualifiedName qualifiedName) { - for (var service : SERVICE_LOADER) { - var result = service.resolveQualifiedName(qualifiedName); - if (result.isPresent()) { - return result; - } - } - return Optional.empty(); - } -} diff --git a/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java index 9db2900e..e1f8d7bc 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java +++ b/subprojects/language/src/main/java/tools/refinery/language/library/RefineryLibrary.java @@ -8,13 +8,18 @@ package tools.refinery.language.library; import org.eclipse.emf.common.util.URI; import org.eclipse.xtext.naming.QualifiedName; +import java.nio.file.Path; import java.util.List; import java.util.Optional; public interface RefineryLibrary { + String EXTENSION = ".refinery"; + default List getAutomaticImports() { return List.of(); } - Optional resolveQualifiedName(QualifiedName qualifiedName); + Optional resolveQualifiedName(QualifiedName qualifiedName, List libraryPaths); + + Optional getQualifiedName(URI uri, List libraryPaths); } diff --git a/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java index 1e78cee1..b3931401 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/naming/ProblemDelegateQualifiedNameProvider.java @@ -5,14 +5,40 @@ */ package tools.refinery.language.naming; +import com.google.inject.Inject; import org.eclipse.xtext.naming.DefaultDeclarativeQualifiedNameProvider; +import org.eclipse.xtext.naming.IQualifiedNameConverter; import org.eclipse.xtext.naming.QualifiedName; import tools.refinery.language.model.problem.Problem; +import tools.refinery.language.scoping.imports.ImportAdapter; +import tools.refinery.language.utils.ProblemUtil; public class ProblemDelegateQualifiedNameProvider extends DefaultDeclarativeQualifiedNameProvider { - protected QualifiedName qualifiedName(Problem ele) { - var qualifiedName = computeFullyQualifiedNameFromNameAttribute(ele); - // Strip the root prefix even if explicitly provided. - return NamingUtil.stripRootPrefix(qualifiedName); + @Inject + private IQualifiedNameConverter qualifiedNameConverter; + + protected QualifiedName qualifiedName(Problem problem) { + var qualifiedNameString = problem.getName(); + if (qualifiedNameString != null) { + return NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(qualifiedNameString)); + } + if (!ProblemUtil.isModule(problem)) { + return null; + } + var resource = problem.eResource(); + if (resource == null) { + return null; + } + var resourceUri = resource.getURI(); + if (resourceUri == null) { + return null; + } + var resourceSet = resource.getResourceSet(); + if (resourceSet == null) { + return null; + } + var adapter = ImportAdapter.getOrInstall(resourceSet); + // If a module has no explicitly specified name, return the qualified name it was resolved under. + return adapter.getQualifiedName(resourceUri); } } 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 a2ec7ed0..7d90ea00 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 @@ -8,14 +8,17 @@ package tools.refinery.language.resource; import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import com.google.inject.Singleton; +import com.google.inject.name.Named; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtext.EcoreUtil2; import org.eclipse.xtext.naming.IQualifiedNameConverter; +import org.eclipse.xtext.naming.IQualifiedNameProvider; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.resource.EObjectDescription; import org.eclipse.xtext.resource.IEObjectDescription; import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; import org.eclipse.xtext.util.IAcceptor; +import tools.refinery.language.naming.ProblemQualifiedNameProvider; import tools.refinery.language.scoping.imports.ImportCollector; import tools.refinery.language.model.problem.*; import tools.refinery.language.naming.NamingUtil; @@ -45,6 +48,10 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti @Inject private IQualifiedNameConverter qualifiedNameConverter; + @Inject + @Named(ProblemQualifiedNameProvider.NAMED_DELEGATE) + private IQualifiedNameProvider delegateQualifiedNameProvider; + @Inject private ImportCollector importCollector; @@ -53,17 +60,17 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti if (!shouldExport(eObject)) { return false; } - var qualifiedName = getNameAsQualifiedName(eObject); - if (qualifiedName == null) { - return true; - } var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); + var problemQualifiedName = getProblemQualifiedName(problem); var userData = getUserData(eObject); if (eObject.equals(problem)) { - acceptEObjectDescription(eObject, qualifiedName, QualifiedName.EMPTY, userData, true, acceptor); + acceptEObjectDescription(eObject, problemQualifiedName, QualifiedName.EMPTY, userData, true, acceptor); + return true; + } + var qualifiedName = getNameAsQualifiedName(eObject); + if (qualifiedName == null) { return true; } - var problemQualifiedName = getNameAsQualifiedName(problem); QualifiedName lastQualifiedNameToExport = null; if (shouldExportSimpleName(eObject)) { lastQualifiedNameToExport = qualifiedName; @@ -107,6 +114,14 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti return qualifiedName; } + protected QualifiedName getProblemQualifiedName(Problem problem) { + if (problem == null) { + return QualifiedName.EMPTY; + } + var qualifiedName = delegateQualifiedNameProvider.getFullyQualifiedName(problem); + return qualifiedName == null ? QualifiedName.EMPTY : qualifiedName; + } + public static boolean shouldExport(EObject eObject) { if (eObject instanceof Variable) { // Variables are always private to the containing predicate definition. diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java index 3e00b87e..610efc03 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/ProblemLocalScopeProvider.java @@ -6,6 +6,7 @@ package tools.refinery.language.scoping; import com.google.inject.Inject; +import com.google.inject.name.Named; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.resource.Resource; @@ -18,13 +19,14 @@ import org.eclipse.xtext.scoping.IScope; import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeDelegatingScopeProvider; import org.eclipse.xtext.scoping.impl.SelectableBasedScope; import org.eclipse.xtext.util.IResourceScopeCache; -import tools.refinery.language.naming.NamingUtil; +import tools.refinery.language.naming.ProblemQualifiedNameProvider; public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScopeProvider { private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemLocalScopeProvider.CACHE_KEY"; @Inject - private IQualifiedNameProvider qualifiedNameProvider; + @Named(ProblemQualifiedNameProvider.NAMED_DELEGATE) + private IQualifiedNameProvider delegateQualifiedNameProvider; @Inject private IResourceDescriptionsProvider resourceDescriptionsProvider; @@ -64,7 +66,7 @@ public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScop if (rootElement == null) { return new LocalImports(resourceDescription, null); } - var rootName = NamingUtil.stripRootPrefix(qualifiedNameProvider.getFullyQualifiedName(rootElement)); + var rootName = delegateQualifiedNameProvider.getFullyQualifiedName(rootElement); if (rootName == null) { return new LocalImports(resourceDescription, null); } diff --git a/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java new file mode 100644 index 00000000..5a8f7fd7 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/scoping/imports/ImportAdapter.java @@ -0,0 +1,230 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.scoping.imports; + +import com.google.common.base.Splitter; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.apache.log4j.Logger; +import org.eclipse.emf.common.notify.Notification; +import org.eclipse.emf.common.notify.impl.AdapterImpl; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.xtext.naming.QualifiedName; +import tools.refinery.language.library.RefineryLibrary; + +import java.io.File; +import java.nio.file.Path; +import java.util.*; + +public class ImportAdapter extends AdapterImpl { + private static final Logger LOG = Logger.getLogger(ImportAdapter.class); + private static final List DEFAULT_LIBRARIES; + private static final List DEFAULT_PATHS; + + static { + var serviceLoader = ServiceLoader.load(RefineryLibrary.class); + var defaultLibraries = new ArrayList(); + for (var service : serviceLoader) { + defaultLibraries.add(service); + } + DEFAULT_LIBRARIES = List.copyOf(defaultLibraries); + var pathEnv = System.getenv("REFINERY_LIBRARY_PATH"); + if (pathEnv == null) { + DEFAULT_PATHS = List.of(); + } else { + DEFAULT_PATHS = Splitter.on(File.pathSeparatorChar) + .splitToStream(pathEnv) + .map(pathString -> Path.of(pathString).toAbsolutePath().normalize()) + .toList(); + } + } + + private final List libraries; + private final List libraryPaths; + private final Cache failedResolutions = + CacheBuilder.newBuilder().maximumSize(100).build(); + private final Map qualifiedNameToUriMap = new LinkedHashMap<>(); + private final Map uriToQualifiedNameMap = new LinkedHashMap<>(); + + private ImportAdapter(ResourceSet resourceSet) { + libraries = new ArrayList<>(DEFAULT_LIBRARIES); + libraryPaths = new ArrayList<>(DEFAULT_PATHS); + for (var resource : resourceSet.getResources()) { + resourceAdded(resource); + } + } + + @Override + public boolean isAdapterForType(Object type) { + return type == ImportAdapter.class; + } + + public List getLibraries() { + return libraries; + } + + public List getLibraryPaths() { + return libraryPaths; + } + + public URI resolveQualifiedName(QualifiedName qualifiedName) { + var uri = getResolvedUri(qualifiedName); + if (uri != null) { + return uri; + } + if (isFailed(qualifiedName)) { + return null; + } + for (var library : libraries) { + var result = library.resolveQualifiedName(qualifiedName, libraryPaths); + if (result.isPresent()) { + uri = result.get(); + markAsResolved(qualifiedName, uri); + return uri; + } + } + markAsUnresolved(qualifiedName); + return null; + } + + private URI getResolvedUri(QualifiedName qualifiedName) { + return qualifiedNameToUriMap.get(qualifiedName); + } + + private boolean isFailed(QualifiedName qualifiedName) { + return failedResolutions.getIfPresent(qualifiedName) != null; + } + + private void markAsResolved(QualifiedName qualifiedName, URI uri) { + if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) { + throw new IllegalArgumentException("Already resolved " + qualifiedName); + } + // We don't need to signal an error here, because modules with multiple qualified names will lead to + // validation errors later. + uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName); + failedResolutions.invalidate(qualifiedName); + } + + private void markAsUnresolved(QualifiedName qualifiedName) { + failedResolutions.put(qualifiedName, qualifiedName); + } + + public QualifiedName getQualifiedName(URI uri) { + return uriToQualifiedNameMap.get(uri); + } + + @Override + public void notifyChanged(Notification msg) { + switch (msg.getEventType()) { + case Notification.ADD -> { + if (msg.getNewValue() instanceof Resource resource) { + resourceAdded(resource); + } + } + case Notification.ADD_MANY -> { + if (msg.getNewValue() instanceof List list) { + manyResourcesAdded(list); + } + } + case Notification.REMOVE -> { + if (msg.getOldValue() instanceof Resource resource) { + resourceRemoved(resource); + } + } + case Notification.REMOVE_MANY -> { + if (msg.getOldValue() instanceof List list) { + manyResourcesRemoved(list); + } + } + default -> { + // Nothing to update. + } + } + } + + private void manyResourcesAdded(List list) { + for (var element : list) { + if (element instanceof Resource resource) { + resourceAdded(resource); + } + } + } + + private void manyResourcesRemoved(List list) { + for (var element : list) { + if (element instanceof Resource resource) { + resourceRemoved(resource); + } + } + } + + private void resourceAdded(Resource resource) { + var uri = resource.getURI(); + for (var library : libraries) { + var result = library.getQualifiedName(uri, libraryPaths); + if (result.isPresent()) { + var qualifiedName = result.get(); + var previousQualifiedName = uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName); + if (previousQualifiedName == null) { + if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) { + throw new IllegalArgumentException("Duplicate resource for" + qualifiedName); + } + } else if (!previousQualifiedName.equals(qualifiedName)) { + LOG.warn("Expected %s to have qualified name %s, got %s instead".formatted( + uri, previousQualifiedName, qualifiedName)); + } + } + } + } + + private void resourceRemoved(Resource resource) { + var qualifiedName = uriToQualifiedNameMap.remove(resource.getURI()); + if (qualifiedName != null) { + qualifiedNameToUriMap.remove(qualifiedName); + } + } + + public static ImportAdapter getOrInstall(ResourceSet resourceSet) { + var adapter = getAdapter(resourceSet); + if (adapter == null) { + adapter = new ImportAdapter(resourceSet); + resourceSet.eAdapters().add(adapter); + } + return adapter; + } + + private static ImportAdapter getAdapter(ResourceSet resourceSet) { + return (ImportAdapter) EcoreUtil.getAdapter(resourceSet.eAdapters(), ImportAdapter.class); + } + + public static void copySettings(EObject context, ResourceSet newResourceSet) { + var resource = context.eResource(); + if (resource == null) { + return; + } + var originalResourceSet = resource.getResourceSet(); + if (originalResourceSet == null) { + return; + } + copySettings(originalResourceSet, newResourceSet); + } + + public static void copySettings(ResourceSet originalResourceSet, ResourceSet newResourceSet) { + var originalAdapter = getAdapter(originalResourceSet); + if (originalAdapter == null) { + return; + } + var newAdapter = getOrInstall(newResourceSet); + newAdapter.libraries.clear(); + newAdapter.libraries.addAll(originalAdapter.libraries); + newAdapter.libraryPaths.clear(); + newAdapter.libraryPaths.addAll(originalAdapter.libraryPaths); + } +} 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 cea99f0a..6cdfa63e 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 @@ -18,7 +18,6 @@ import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.resource.IEObjectDescription; import org.eclipse.xtext.util.IResourceScopeCache; -import tools.refinery.language.library.RefineryLibraries; import tools.refinery.language.model.problem.ImportStatement; import tools.refinery.language.model.problem.Problem; import tools.refinery.language.model.problem.ProblemPackage; @@ -54,28 +53,49 @@ public class ImportCollector { if (resource.getContents().isEmpty() || !(resource.getContents().getFirst() instanceof Problem problem)) { return ImportCollection.EMPTY; } + var resourceSet = resource.getResourceSet(); + if (resourceSet == null) { + return ImportCollection.EMPTY; + } Map> aliasesMap = new LinkedHashMap<>(); for (var statement : problem.getStatements()) { if (statement instanceof ImportStatement importStatement) { collectImportStatement(importStatement, aliasesMap); } } + var adapter = ImportAdapter.getOrInstall(resourceSet); var collection = new ImportCollection(); - collection.addAll(RefineryLibraries.getAutomaticImports()); + collectAutomaticImports(collection, adapter); + collectExplicitImports(aliasesMap, collection, adapter); + collection.remove(resource.getURI()); + return collection; + } + + private void collectAutomaticImports(ImportCollection importCollection, ImportAdapter adapter) { + for (var library : adapter.getLibraries()) { + for (var qualifiedName : library.getAutomaticImports()) { + var uri = adapter.resolveQualifiedName(qualifiedName); + if (uri != null) { + importCollection.add(NamedImport.implicit(uri, qualifiedName)); + } + } + } + } + + private void collectExplicitImports(Map> aliasesMap, + ImportCollection collection, ImportAdapter adapter) { for (var entry : aliasesMap.entrySet()) { var qualifiedName = entry.getKey(); - RefineryLibraries.resolveQualifiedName(qualifiedName).ifPresent(uri -> { - if (!uri.equals(resource.getURI())) { - var aliases = entry.getValue(); - collection.add(NamedImport.explicit(uri, qualifiedName, List.copyOf(aliases))); - } - }); + var uri = adapter.resolveQualifiedName(qualifiedName); + if (uri != null) { + var aliases = entry.getValue(); + collection.add(NamedImport.explicit(uri, qualifiedName, List.copyOf(aliases))); + } } - collection.remove(resource.getURI()); - return collection; } - private void collectImportStatement(ImportStatement importStatement, Map> aliasesMap) { + private void collectImportStatement(ImportStatement importStatement, + Map> aliasesMap) { var nodes = NodeModelUtils.findNodesForFeature(importStatement, ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE); var aliasString = importStatement.getAlias(); 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 23ff55e7..6b48cb5a 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 @@ -124,8 +124,12 @@ public final class ProblemUtil { }; } + public static boolean isModule(Problem problem) { + return problem.getKind() == ModuleKind.MODULE; + } + public static boolean isInModule(EObject eObject) { var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); - return problem != null && problem.getKind() == ModuleKind.MODULE; + return problem != null && isModule(problem); } } diff --git a/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary b/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary index bb7e369d..8e454ee5 100644 --- a/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary +++ b/subprojects/language/src/main/resources/META-INF/services/tools.refinery.language.library.RefineryLibrary @@ -2,3 +2,4 @@ # # SPDX-License-Identifier: EPL-2.0 tools.refinery.language.library.BuiltinLibrary +tools.refinery.language.library.PathLibrary diff --git a/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery b/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery index 022c3167..f9ef959d 100644 --- a/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery +++ b/subprojects/language/src/main/resources/tools/refinery/language/library/builtin.refinery @@ -1,7 +1,7 @@ -% SPDX-FileCopyrightText: 2021-2023 The Refinery Authors +% SPDX-FileCopyrightText: 2021-2024 The Refinery Authors % % SPDX-License-Identifier: EPL-2.0 -problem builtin. +module builtin. abstract class node. -- cgit v1.2.3-70-g09d2