diff options
author | Kristóf Marussy <kristof@marussy.com> | 2024-02-04 22:06:56 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2024-02-04 22:06:56 +0100 |
commit | 4dd2b4e1f45bf940d969181ae9b9176fa353c92e (patch) | |
tree | 72e315d726014966d8c701686962bfb5fbdea5a7 | |
parent | feat(language): import resolution (diff) | |
download | refinery-4dd2b4e1f45bf940d969181ae9b9176fa353c92e.tar.gz refinery-4dd2b4e1f45bf940d969181ae9b9176fa353c92e.tar.zst refinery-4dd2b4e1f45bf940d969181ae9b9176fa353c92e.zip |
feat: filesystem-level import resolution
Modules without an explicitly declared name get a name automatically inferred
from their path.
17 files changed, 556 insertions, 112 deletions
diff --git a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java index b33fce23..b1c28df0 100644 --- a/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java +++ b/subprojects/generator-cli/src/main/java/tools/refinery/generator/cli/commands/GenerateCommand.java | |||
@@ -58,6 +58,7 @@ public class GenerateCommand { | |||
58 | } | 58 | } |
59 | 59 | ||
60 | public void run() throws IOException { | 60 | public void run() throws IOException { |
61 | loader.extraPath(System.getProperty("user.dir")); | ||
61 | var problem = isStandardStream(inputPath) ? loader.loadStream(System.in) : loader.loadFile(inputPath); | 62 | var problem = isStandardStream(inputPath) ? loader.loadStream(System.in) : loader.loadFile(inputPath); |
62 | problem = loader.loadScopeConstraints(problem, scopes, overrideScopes); | 63 | problem = loader.loadScopeConstraints(problem, scopes, overrideScopes); |
63 | var generator = generatorFactory.createGenerator(problem); | 64 | var generator = generatorFactory.createGenerator(problem); |
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 20ea8132..e44dddc0 100644 --- a/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java +++ b/subprojects/generator/src/main/java/tools/refinery/generator/ProblemLoader.java | |||
@@ -1,5 +1,5 @@ | |||
1 | /* | 1 | /* |
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | 2 | * SPDX-FileCopyrightText: 2023-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 | */ |
@@ -19,6 +19,7 @@ import org.eclipse.xtext.validation.IResourceValidator; | |||
19 | import tools.refinery.language.model.problem.Problem; | 19 | import tools.refinery.language.model.problem.Problem; |
20 | import tools.refinery.language.model.problem.Relation; | 20 | import tools.refinery.language.model.problem.Relation; |
21 | import tools.refinery.language.model.problem.ScopeDeclaration; | 21 | import tools.refinery.language.model.problem.ScopeDeclaration; |
22 | import tools.refinery.language.scoping.imports.ImportAdapter; | ||
22 | import tools.refinery.store.util.CancellationToken; | 23 | import tools.refinery.store.util.CancellationToken; |
23 | 24 | ||
24 | import java.io.ByteArrayOutputStream; | 25 | import java.io.ByteArrayOutputStream; |
@@ -26,11 +27,14 @@ import java.io.File; | |||
26 | import java.io.IOException; | 27 | import java.io.IOException; |
27 | import java.io.InputStream; | 28 | import java.io.InputStream; |
28 | import java.nio.charset.StandardCharsets; | 29 | import java.nio.charset.StandardCharsets; |
30 | import java.nio.file.Path; | ||
29 | import java.util.ArrayList; | 31 | import java.util.ArrayList; |
30 | import java.util.HashSet; | 32 | import java.util.HashSet; |
31 | import java.util.List; | 33 | import java.util.List; |
32 | import java.util.Map; | 34 | import java.util.Map; |
33 | 35 | ||
36 | // This class is used as a fluent builder. | ||
37 | @SuppressWarnings("UnusedReturnValue") | ||
34 | public class ProblemLoader { | 38 | public class ProblemLoader { |
35 | private String fileExtension; | 39 | private String fileExtension; |
36 | 40 | ||
@@ -45,6 +49,8 @@ public class ProblemLoader { | |||
45 | 49 | ||
46 | private CancellationToken cancellationToken = CancellationToken.NONE; | 50 | private CancellationToken cancellationToken = CancellationToken.NONE; |
47 | 51 | ||
52 | private final List<Path> extraPaths = new ArrayList<>(); | ||
53 | |||
48 | @Inject | 54 | @Inject |
49 | public void setFileExtensionProvider(FileExtensionProvider fileExtensionProvider) { | 55 | public void setFileExtensionProvider(FileExtensionProvider fileExtensionProvider) { |
50 | this.fileExtension = fileExtensionProvider.getPrimaryFileExtension(); | 56 | this.fileExtension = fileExtensionProvider.getPrimaryFileExtension(); |
@@ -55,6 +61,15 @@ public class ProblemLoader { | |||
55 | return this; | 61 | return this; |
56 | } | 62 | } |
57 | 63 | ||
64 | public ProblemLoader extraPath(String path) { | ||
65 | return extraPath(Path.of(path)); | ||
66 | } | ||
67 | |||
68 | public ProblemLoader extraPath(Path path) { | ||
69 | extraPaths.add(path.toAbsolutePath().normalize()); | ||
70 | return this; | ||
71 | } | ||
72 | |||
58 | public Problem loadString(String problemString, URI uri) throws IOException { | 73 | public Problem loadString(String problemString, URI uri) throws IOException { |
59 | try (var stream = new LazyStringInputStream(problemString)) { | 74 | try (var stream = new LazyStringInputStream(problemString)) { |
60 | return loadStream(stream, uri); | 75 | return loadStream(stream, uri); |
@@ -66,8 +81,8 @@ public class ProblemLoader { | |||
66 | } | 81 | } |
67 | 82 | ||
68 | public Problem loadStream(InputStream inputStream, URI uri) throws IOException { | 83 | public Problem loadStream(InputStream inputStream, URI uri) throws IOException { |
69 | var resourceSet = resourceSetProvider.get(); | 84 | var resourceSet = createResourceSet(); |
70 | var resourceUri = uri == null ? URI.createFileURI("__synthetic." + fileExtension) : uri; | 85 | var resourceUri = uri == null ? URI.createURI("__synthetic." + fileExtension) : uri; |
71 | var resource = resourceFactory.createResource(resourceUri); | 86 | var resource = resourceFactory.createResource(resourceUri); |
72 | resourceSet.getResources().add(resource); | 87 | resourceSet.getResources().add(resource); |
73 | resource.load(inputStream, Map.of()); | 88 | resource.load(inputStream, Map.of()); |
@@ -87,13 +102,20 @@ public class ProblemLoader { | |||
87 | } | 102 | } |
88 | 103 | ||
89 | public Problem loadUri(URI uri) throws IOException { | 104 | public Problem loadUri(URI uri) throws IOException { |
90 | var resourceSet = resourceSetProvider.get(); | 105 | var resourceSet = createResourceSet(); |
91 | var resource = resourceFactory.createResource(uri); | 106 | var resource = resourceFactory.createResource(uri); |
92 | resourceSet.getResources().add(resource); | 107 | resourceSet.getResources().add(resource); |
93 | resource.load(Map.of()); | 108 | resource.load(Map.of()); |
94 | return loadResource(resource); | 109 | return loadResource(resource); |
95 | } | 110 | } |
96 | 111 | ||
112 | private XtextResourceSet createResourceSet() { | ||
113 | var resourceSet = resourceSetProvider.get(); | ||
114 | var adapter = ImportAdapter.getOrInstall(resourceSet); | ||
115 | adapter.getLibraryPaths().addAll(0, extraPaths); | ||
116 | return resourceSet; | ||
117 | } | ||
118 | |||
97 | public Problem loadResource(Resource resource) { | 119 | public Problem loadResource(Resource resource) { |
98 | var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> { | 120 | var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> { |
99 | cancellationToken.checkCancelled(); | 121 | cancellationToken.checkCancelled(); |
diff --git a/subprojects/language-model/problem.aird b/subprojects/language-model/problem.aird index 6583d364..ed3a4b58 100644 --- a/subprojects/language-model/problem.aird +++ b/subprojects/language-model/problem.aird | |||
@@ -7,7 +7,7 @@ | |||
7 | <semanticResources>build/resources/main/model/problem.genmodel</semanticResources> | 7 | <semanticResources>build/resources/main/model/problem.genmodel</semanticResources> |
8 | <ownedViews xmi:type="viewpoint:DView" uid="_CsAAYKA4EeuqkpDnuik1sg"> | 8 | <ownedViews xmi:type="viewpoint:DView" uid="_CsAAYKA4EeuqkpDnuik1sg"> |
9 | <viewpoint xmi:type="description:Viewpoint" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']"/> | 9 | <viewpoint xmi:type="description:Viewpoint" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']"/> |
10 | <ownedRepresentationDescriptors xmi:type="viewpoint:DRepresentationDescriptor" uid="_CsYa4KA4EeuqkpDnuik1sg" name="declarations" repPath="#_CsUwgKA4EeuqkpDnuik1sg" changeId="1706722245720"> | 10 | <ownedRepresentationDescriptors xmi:type="viewpoint:DRepresentationDescriptor" uid="_CsYa4KA4EeuqkpDnuik1sg" name="declarations" repPath="#_CsUwgKA4EeuqkpDnuik1sg" changeId="1707054359028"> |
11 | <description xmi:type="description_1:DiagramDescription" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']"/> | 11 | <description xmi:type="description_1:DiagramDescription" href="platform:/plugin/org.eclipse.emf.ecoretools.design/description/ecore.odesign#//@ownedViewpoints[name='Design']/@ownedRepresentations[name='Entities']"/> |
12 | <target xmi:type="ecore:EPackage" href="src/main/resources/model/problem.ecore#/"/> | 12 | <target xmi:type="ecore:EPackage" href="src/main/resources/model/problem.ecore#/"/> |
13 | </ownedRepresentationDescriptors> | 13 | </ownedRepresentationDescriptors> |
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/SolutionSerializer.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/SolutionSerializer.java index 1bd419d8..dad867b8 100644 --- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/SolutionSerializer.java +++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/SolutionSerializer.java | |||
@@ -19,6 +19,7 @@ import org.eclipse.xtext.resource.XtextResource; | |||
19 | import org.eclipse.xtext.resource.XtextResourceSet; | 19 | import org.eclipse.xtext.resource.XtextResourceSet; |
20 | import org.eclipse.xtext.scoping.IScopeProvider; | 20 | import org.eclipse.xtext.scoping.IScopeProvider; |
21 | import tools.refinery.language.model.problem.*; | 21 | import tools.refinery.language.model.problem.*; |
22 | import tools.refinery.language.scoping.imports.ImportAdapter; | ||
22 | import tools.refinery.language.utils.ProblemDesugarer; | 23 | import tools.refinery.language.utils.ProblemDesugarer; |
23 | import tools.refinery.language.utils.ProblemUtil; | 24 | import tools.refinery.language.utils.ProblemUtil; |
24 | import tools.refinery.store.model.Model; | 25 | import tools.refinery.store.model.Model; |
@@ -79,7 +80,7 @@ public class SolutionSerializer { | |||
79 | } | 80 | } |
80 | 81 | ||
81 | public Problem serializeSolution(ProblemTrace trace, Model model) { | 82 | public Problem serializeSolution(ProblemTrace trace, Model model) { |
82 | var uri = URI.createFileURI("__synthetic" + fileExtension); | 83 | var uri = URI.createURI("__synthetic." + fileExtension); |
83 | return serializeSolution(trace, model, uri); | 84 | return serializeSolution(trace, model, uri); |
84 | } | 85 | } |
85 | 86 | ||
@@ -133,6 +134,7 @@ public class SolutionSerializer { | |||
133 | 134 | ||
134 | private Problem copyProblem(Problem originalProblem, URI uri) { | 135 | private Problem copyProblem(Problem originalProblem, URI uri) { |
135 | var newResourceSet = resourceSetProvider.get(); | 136 | var newResourceSet = resourceSetProvider.get(); |
137 | ImportAdapter.copySettings(originalProblem, newResourceSet); | ||
136 | if (!fileExtension.equals(uri.fileExtension())) { | 138 | if (!fileExtension.equals(uri.fileExtension())) { |
137 | uri = uri.appendFileExtension(fileExtension); | 139 | uri = uri.appendFileExtension(fileExtension); |
138 | } | 140 | } |
@@ -153,6 +155,7 @@ public class SolutionSerializer { | |||
153 | throw new IllegalStateException("Failed to copy problem", e); | 155 | throw new IllegalStateException("Failed to copy problem", e); |
154 | } | 156 | } |
155 | var contents = newResource.getContents(); | 157 | var contents = newResource.getContents(); |
158 | EcoreUtil.resolveAll(newResourceSet); | ||
156 | if (!contents.isEmpty() && contents.getFirst() instanceof Problem newProblem) { | 159 | if (!contents.isEmpty() && contents.getFirst() instanceof Problem newProblem) { |
157 | return newProblem; | 160 | return newProblem; |
158 | } | 161 | } |
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; | |||
9 | import org.eclipse.xtext.naming.QualifiedName; | 9 | import org.eclipse.xtext.naming.QualifiedName; |
10 | 10 | ||
11 | import java.util.List; | 11 | import java.util.List; |
12 | import java.util.Optional; | ||
13 | 12 | ||
14 | public class BuiltinLibrary implements RefineryLibrary { | 13 | public class BuiltinLibrary extends ClasspathBasedLibrary { |
15 | public static final QualifiedName BUILTIN_LIBRARY_NAME = QualifiedName.create("builtin"); | 14 | public static final QualifiedName BUILTIN_LIBRARY_NAME = QualifiedName.create("builtin"); |
16 | public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME).orElseThrow( | 15 | public static final URI BUILTIN_LIBRARY_URI = ClasspathBasedLibrary.getLibraryUri( |
16 | BuiltinLibrary.class, BUILTIN_LIBRARY_NAME).orElseThrow( | ||
17 | () -> new IllegalStateException("Builtin library was not found")); | 17 | () -> new IllegalStateException("Builtin library was not found")); |
18 | 18 | ||
19 | @Override | 19 | public BuiltinLibrary() { |
20 | public List<QualifiedName> getAutomaticImports() { | 20 | super(BUILTIN_LIBRARY_NAME, List.of(BUILTIN_LIBRARY_NAME)); |
21 | return List.of(BUILTIN_LIBRARY_NAME); | ||
22 | } | ||
23 | |||
24 | @Override | ||
25 | public Optional<URI> resolveQualifiedName(QualifiedName qualifiedName) { | ||
26 | if (qualifiedName.startsWith(BUILTIN_LIBRARY_NAME)) { | ||
27 | return getLibraryUri(qualifiedName); | ||
28 | } | ||
29 | return Optional.empty(); | ||
30 | } | ||
31 | |||
32 | private static Optional<URI> getLibraryUri(QualifiedName qualifiedName) { | ||
33 | var libraryPath = String.join("/", qualifiedName.getSegments()); | ||
34 | var libraryResource = BuiltinLibrary.class.getClassLoader() | ||
35 | .getResource("tools/refinery/language/library/%s.refinery".formatted(libraryPath)); | ||
36 | if (libraryResource == null) { | ||
37 | return Optional.empty(); | ||
38 | } | ||
39 | return Optional.of(URI.createURI(libraryResource.toString())); | ||
40 | } | 21 | } |
41 | } | 22 | } |
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.library; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | |||
11 | import java.nio.file.Path; | ||
12 | import java.util.*; | ||
13 | |||
14 | public abstract class ClasspathBasedLibrary implements RefineryLibrary { | ||
15 | private final QualifiedName prefix; | ||
16 | private final List<QualifiedName> automaticImports; | ||
17 | private final URI rootUri; | ||
18 | |||
19 | protected ClasspathBasedLibrary(QualifiedName prefix, List<QualifiedName> automaticImports) { | ||
20 | this.prefix = prefix; | ||
21 | this.automaticImports = List.copyOf(automaticImports); | ||
22 | var context = this.getClass(); | ||
23 | var contextPath = context.getCanonicalName().replace('.', '/') + ".class"; | ||
24 | var contextResource = context.getClassLoader().getResource(contextPath); | ||
25 | if (contextResource == null) { | ||
26 | throw new IllegalStateException("Failed to find library context"); | ||
27 | } | ||
28 | var contextUri = URI.createURI(contextResource.toString()); | ||
29 | var segments = Arrays.copyOf(contextUri.segments(), contextUri.segmentCount() - 1); | ||
30 | rootUri = URI.createHierarchicalURI(contextUri.scheme(), contextUri.authority(), contextUri.device(), | ||
31 | segments, null, null); | ||
32 | } | ||
33 | |||
34 | protected ClasspathBasedLibrary(QualifiedName prefix) { | ||
35 | this(prefix, List.of()); | ||
36 | } | ||
37 | |||
38 | @Override | ||
39 | public List<QualifiedName> getAutomaticImports() { | ||
40 | return automaticImports; | ||
41 | } | ||
42 | |||
43 | @Override | ||
44 | public Optional<URI> resolveQualifiedName(QualifiedName qualifiedName, List<Path> libraryPaths) { | ||
45 | if (qualifiedName.startsWith(prefix)) { | ||
46 | return getLibraryUri(this.getClass(), qualifiedName); | ||
47 | } | ||
48 | return Optional.empty(); | ||
49 | } | ||
50 | |||
51 | @Override | ||
52 | public Optional<QualifiedName> getQualifiedName(URI uri, List<Path> libraryPaths) { | ||
53 | if (!uri.isHierarchical() || | ||
54 | !Objects.equals(rootUri.scheme(), uri.scheme()) || | ||
55 | !Objects.equals(rootUri.authority(), uri.authority()) || | ||
56 | !Objects.equals(rootUri.device(), uri.device()) || | ||
57 | rootUri.segmentCount() >= uri.segmentCount()) { | ||
58 | return Optional.empty(); | ||
59 | } | ||
60 | int rootSegmentCount = rootUri.segmentCount(); | ||
61 | int uriSegmentCount = uri.segmentCount(); | ||
62 | if (!uri.segment(uriSegmentCount - 1).endsWith(RefineryLibrary.EXTENSION)) { | ||
63 | return Optional.empty(); | ||
64 | } | ||
65 | var segments = new ArrayList<String>(); | ||
66 | int i = 0; | ||
67 | while (i < rootSegmentCount) { | ||
68 | if (!rootUri.segment(i).equals(uri.segment(i))) { | ||
69 | return Optional.empty(); | ||
70 | } | ||
71 | i++; | ||
72 | } | ||
73 | while (i < uriSegmentCount) { | ||
74 | var segment = uri.segment(i); | ||
75 | if (i == uriSegmentCount - 1) { | ||
76 | segment = segment.substring(0, segment.length() - RefineryLibrary.EXTENSION.length()); | ||
77 | } | ||
78 | segments.add(segment); | ||
79 | i++; | ||
80 | } | ||
81 | var qualifiedName = QualifiedName.create(segments); | ||
82 | if (!qualifiedName.startsWith(prefix)) { | ||
83 | return Optional.empty(); | ||
84 | } | ||
85 | return Optional.of(qualifiedName); | ||
86 | } | ||
87 | |||
88 | public static Optional<URI> getLibraryUri(Class<?> context, QualifiedName qualifiedName) { | ||
89 | var packagePath = context.getPackageName().replace('.', '/'); | ||
90 | var libraryPath = String.join("/", qualifiedName.getSegments()); | ||
91 | var resourceName = "%s/%s%s".formatted(packagePath, libraryPath, RefineryLibrary.EXTENSION); | ||
92 | var resource = context.getClassLoader().getResource(resourceName); | ||
93 | if (resource == null) { | ||
94 | return Optional.empty(); | ||
95 | } | ||
96 | return Optional.of(URI.createURI(resource.toString())); | ||
97 | } | ||
98 | } | ||
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.library; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | |||
11 | import java.nio.file.Files; | ||
12 | import java.nio.file.Path; | ||
13 | import java.util.ArrayList; | ||
14 | import java.util.List; | ||
15 | import java.util.Optional; | ||
16 | |||
17 | public final class PathLibrary implements RefineryLibrary { | ||
18 | @Override | ||
19 | public Optional<URI> resolveQualifiedName(QualifiedName qualifiedName, List<Path> libraryPaths) { | ||
20 | if (libraryPaths.isEmpty()) { | ||
21 | return Optional.empty(); | ||
22 | } | ||
23 | if (qualifiedName.getSegmentCount() == 0) { | ||
24 | return Optional.empty(); | ||
25 | } | ||
26 | var relativePath = qualifiedNameToRelativePath(qualifiedName); | ||
27 | for (var library : libraryPaths) { | ||
28 | var absoluteResolvedPath = library.resolve(relativePath).toAbsolutePath().normalize(); | ||
29 | if (absoluteResolvedPath.startsWith(library) && Files.exists(absoluteResolvedPath)) { | ||
30 | var uri = URI.createFileURI(absoluteResolvedPath.toString()); | ||
31 | return Optional.of(uri); | ||
32 | } | ||
33 | } | ||
34 | return Optional.empty(); | ||
35 | } | ||
36 | |||
37 | private static Path qualifiedNameToRelativePath(QualifiedName qualifiedName) { | ||
38 | int segmentCount = qualifiedName.getSegmentCount(); | ||
39 | String first = null; | ||
40 | var rest = new String[segmentCount - 1]; | ||
41 | for (var i = 0; i < segmentCount; i++) { | ||
42 | var segment = qualifiedName.getSegment(i); | ||
43 | if (i == segmentCount - 1) { | ||
44 | segment = segment + RefineryLibrary.EXTENSION; | ||
45 | } | ||
46 | if (i == 0) { | ||
47 | first = segment; | ||
48 | } else { | ||
49 | rest[i - 1] = segment; | ||
50 | } | ||
51 | } | ||
52 | if (first == null) { | ||
53 | throw new AssertionError("Expected qualified name to have non-null segments"); | ||
54 | } | ||
55 | return Path.of(first, rest); | ||
56 | } | ||
57 | |||
58 | @Override | ||
59 | public Optional<QualifiedName> getQualifiedName(URI uri, List<Path> libraryPaths) { | ||
60 | if (libraryPaths.isEmpty()) { | ||
61 | return Optional.empty(); | ||
62 | } | ||
63 | if (!uri.isFile() || !uri.hasAbsolutePath()) { | ||
64 | return Optional.empty(); | ||
65 | } | ||
66 | var path = Path.of(uri.toFileString()); | ||
67 | for (var library : libraryPaths) { | ||
68 | if (path.startsWith(library)) { | ||
69 | return getRelativeQualifiedName(library, path); | ||
70 | } | ||
71 | } | ||
72 | return Optional.empty(); | ||
73 | } | ||
74 | |||
75 | private static Optional<QualifiedName> getRelativeQualifiedName(Path library, Path path) { | ||
76 | var relativePath = path.relativize(library); | ||
77 | var segments = new ArrayList<String>(); | ||
78 | for (Path value : relativePath) { | ||
79 | segments.add(value.toString()); | ||
80 | } | ||
81 | int lastIndex = segments.size() - 1; | ||
82 | var lastSegment = segments.get(lastIndex); | ||
83 | if (!lastSegment.endsWith(EXTENSION)) { | ||
84 | return Optional.empty(); | ||
85 | } | ||
86 | lastSegment = lastSegment.substring(0, lastSegment.length() - RefineryLibrary.EXTENSION.length()); | ||
87 | segments.set(lastIndex, lastSegment); | ||
88 | return Optional.of(QualifiedName.create(segments)); | ||
89 | } | ||
90 | } | ||
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.library; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | ||
10 | import tools.refinery.language.scoping.imports.NamedImport; | ||
11 | |||
12 | import java.util.LinkedHashMap; | ||
13 | import java.util.List; | ||
14 | import java.util.Optional; | ||
15 | import java.util.ServiceLoader; | ||
16 | |||
17 | public final class RefineryLibraries { | ||
18 | private static final ServiceLoader<RefineryLibrary> SERVICE_LOADER = ServiceLoader.load(RefineryLibrary.class); | ||
19 | private static final List<NamedImport> AUTOMATIC_IMPORTS; | ||
20 | |||
21 | static { | ||
22 | var imports = new LinkedHashMap<QualifiedName, URI>(); | ||
23 | for (var service : SERVICE_LOADER) { | ||
24 | for (var qualifiedName : service.getAutomaticImports()) { | ||
25 | var uri = service.resolveQualifiedName(qualifiedName).orElseThrow( | ||
26 | () -> new IllegalStateException("Automatic import %s was not found".formatted(qualifiedName))); | ||
27 | if (imports.put(qualifiedName, uri) != null) { | ||
28 | throw new IllegalStateException("Duplicate automatic import " + qualifiedName); | ||
29 | } | ||
30 | } | ||
31 | } | ||
32 | AUTOMATIC_IMPORTS = imports.entrySet().stream() | ||
33 | .map(entry -> NamedImport.implicit(entry.getValue(), entry.getKey())) | ||
34 | .toList(); | ||
35 | } | ||
36 | |||
37 | private RefineryLibraries() { | ||
38 | throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); | ||
39 | } | ||
40 | |||
41 | public static List<NamedImport> getAutomaticImports() { | ||
42 | return AUTOMATIC_IMPORTS; | ||
43 | } | ||
44 | |||
45 | public static Optional<URI> resolveQualifiedName(QualifiedName qualifiedName) { | ||
46 | for (var service : SERVICE_LOADER) { | ||
47 | var result = service.resolveQualifiedName(qualifiedName); | ||
48 | if (result.isPresent()) { | ||
49 | return result; | ||
50 | } | ||
51 | } | ||
52 | return Optional.empty(); | ||
53 | } | ||
54 | } | ||
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; | |||
8 | import org.eclipse.emf.common.util.URI; | 8 | import org.eclipse.emf.common.util.URI; |
9 | import org.eclipse.xtext.naming.QualifiedName; | 9 | import org.eclipse.xtext.naming.QualifiedName; |
10 | 10 | ||
11 | import java.nio.file.Path; | ||
11 | import java.util.List; | 12 | import java.util.List; |
12 | import java.util.Optional; | 13 | import java.util.Optional; |
13 | 14 | ||
14 | public interface RefineryLibrary { | 15 | public interface RefineryLibrary { |
16 | String EXTENSION = ".refinery"; | ||
17 | |||
15 | default List<QualifiedName> getAutomaticImports() { | 18 | default List<QualifiedName> getAutomaticImports() { |
16 | return List.of(); | 19 | return List.of(); |
17 | } | 20 | } |
18 | 21 | ||
19 | Optional<URI> resolveQualifiedName(QualifiedName qualifiedName); | 22 | Optional<URI> resolveQualifiedName(QualifiedName qualifiedName, List<Path> libraryPaths); |
23 | |||
24 | Optional<QualifiedName> getQualifiedName(URI uri, List<Path> libraryPaths); | ||
20 | } | 25 | } |
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 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.naming; | 6 | package tools.refinery.language.naming; |
7 | 7 | ||
8 | import com.google.inject.Inject; | ||
8 | import org.eclipse.xtext.naming.DefaultDeclarativeQualifiedNameProvider; | 9 | import org.eclipse.xtext.naming.DefaultDeclarativeQualifiedNameProvider; |
10 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
9 | import org.eclipse.xtext.naming.QualifiedName; | 11 | import org.eclipse.xtext.naming.QualifiedName; |
10 | import tools.refinery.language.model.problem.Problem; | 12 | import tools.refinery.language.model.problem.Problem; |
13 | import tools.refinery.language.scoping.imports.ImportAdapter; | ||
14 | import tools.refinery.language.utils.ProblemUtil; | ||
11 | 15 | ||
12 | public class ProblemDelegateQualifiedNameProvider extends DefaultDeclarativeQualifiedNameProvider { | 16 | public class ProblemDelegateQualifiedNameProvider extends DefaultDeclarativeQualifiedNameProvider { |
13 | protected QualifiedName qualifiedName(Problem ele) { | 17 | @Inject |
14 | var qualifiedName = computeFullyQualifiedNameFromNameAttribute(ele); | 18 | private IQualifiedNameConverter qualifiedNameConverter; |
15 | // Strip the root prefix even if explicitly provided. | 19 | |
16 | return NamingUtil.stripRootPrefix(qualifiedName); | 20 | protected QualifiedName qualifiedName(Problem problem) { |
21 | var qualifiedNameString = problem.getName(); | ||
22 | if (qualifiedNameString != null) { | ||
23 | return NamingUtil.stripRootPrefix(qualifiedNameConverter.toQualifiedName(qualifiedNameString)); | ||
24 | } | ||
25 | if (!ProblemUtil.isModule(problem)) { | ||
26 | return null; | ||
27 | } | ||
28 | var resource = problem.eResource(); | ||
29 | if (resource == null) { | ||
30 | return null; | ||
31 | } | ||
32 | var resourceUri = resource.getURI(); | ||
33 | if (resourceUri == null) { | ||
34 | return null; | ||
35 | } | ||
36 | var resourceSet = resource.getResourceSet(); | ||
37 | if (resourceSet == null) { | ||
38 | return null; | ||
39 | } | ||
40 | var adapter = ImportAdapter.getOrInstall(resourceSet); | ||
41 | // If a module has no explicitly specified name, return the qualified name it was resolved under. | ||
42 | return adapter.getQualifiedName(resourceUri); | ||
17 | } | 43 | } |
18 | } | 44 | } |
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; | |||
8 | import com.google.common.collect.ImmutableMap; | 8 | import com.google.common.collect.ImmutableMap; |
9 | import com.google.inject.Inject; | 9 | import com.google.inject.Inject; |
10 | import com.google.inject.Singleton; | 10 | import com.google.inject.Singleton; |
11 | import com.google.inject.name.Named; | ||
11 | import org.eclipse.emf.ecore.EObject; | 12 | import org.eclipse.emf.ecore.EObject; |
12 | import org.eclipse.xtext.EcoreUtil2; | 13 | import org.eclipse.xtext.EcoreUtil2; |
13 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | 14 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
15 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
14 | import org.eclipse.xtext.naming.QualifiedName; | 16 | import org.eclipse.xtext.naming.QualifiedName; |
15 | import org.eclipse.xtext.resource.EObjectDescription; | 17 | import org.eclipse.xtext.resource.EObjectDescription; |
16 | import org.eclipse.xtext.resource.IEObjectDescription; | 18 | import org.eclipse.xtext.resource.IEObjectDescription; |
17 | import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; | 19 | import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; |
18 | import org.eclipse.xtext.util.IAcceptor; | 20 | import org.eclipse.xtext.util.IAcceptor; |
21 | import tools.refinery.language.naming.ProblemQualifiedNameProvider; | ||
19 | import tools.refinery.language.scoping.imports.ImportCollector; | 22 | import tools.refinery.language.scoping.imports.ImportCollector; |
20 | import tools.refinery.language.model.problem.*; | 23 | import tools.refinery.language.model.problem.*; |
21 | import tools.refinery.language.naming.NamingUtil; | 24 | import tools.refinery.language.naming.NamingUtil; |
@@ -46,6 +49,10 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
46 | private IQualifiedNameConverter qualifiedNameConverter; | 49 | private IQualifiedNameConverter qualifiedNameConverter; |
47 | 50 | ||
48 | @Inject | 51 | @Inject |
52 | @Named(ProblemQualifiedNameProvider.NAMED_DELEGATE) | ||
53 | private IQualifiedNameProvider delegateQualifiedNameProvider; | ||
54 | |||
55 | @Inject | ||
49 | private ImportCollector importCollector; | 56 | private ImportCollector importCollector; |
50 | 57 | ||
51 | @Override | 58 | @Override |
@@ -53,17 +60,17 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
53 | if (!shouldExport(eObject)) { | 60 | if (!shouldExport(eObject)) { |
54 | return false; | 61 | return false; |
55 | } | 62 | } |
56 | var qualifiedName = getNameAsQualifiedName(eObject); | ||
57 | if (qualifiedName == null) { | ||
58 | return true; | ||
59 | } | ||
60 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); | 63 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); |
64 | var problemQualifiedName = getProblemQualifiedName(problem); | ||
61 | var userData = getUserData(eObject); | 65 | var userData = getUserData(eObject); |
62 | if (eObject.equals(problem)) { | 66 | if (eObject.equals(problem)) { |
63 | acceptEObjectDescription(eObject, qualifiedName, QualifiedName.EMPTY, userData, true, acceptor); | 67 | acceptEObjectDescription(eObject, problemQualifiedName, QualifiedName.EMPTY, userData, true, acceptor); |
68 | return true; | ||
69 | } | ||
70 | var qualifiedName = getNameAsQualifiedName(eObject); | ||
71 | if (qualifiedName == null) { | ||
64 | return true; | 72 | return true; |
65 | } | 73 | } |
66 | var problemQualifiedName = getNameAsQualifiedName(problem); | ||
67 | QualifiedName lastQualifiedNameToExport = null; | 74 | QualifiedName lastQualifiedNameToExport = null; |
68 | if (shouldExportSimpleName(eObject)) { | 75 | if (shouldExportSimpleName(eObject)) { |
69 | lastQualifiedNameToExport = qualifiedName; | 76 | lastQualifiedNameToExport = qualifiedName; |
@@ -107,6 +114,14 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
107 | return qualifiedName; | 114 | return qualifiedName; |
108 | } | 115 | } |
109 | 116 | ||
117 | protected QualifiedName getProblemQualifiedName(Problem problem) { | ||
118 | if (problem == null) { | ||
119 | return QualifiedName.EMPTY; | ||
120 | } | ||
121 | var qualifiedName = delegateQualifiedNameProvider.getFullyQualifiedName(problem); | ||
122 | return qualifiedName == null ? QualifiedName.EMPTY : qualifiedName; | ||
123 | } | ||
124 | |||
110 | public static boolean shouldExport(EObject eObject) { | 125 | public static boolean shouldExport(EObject eObject) { |
111 | if (eObject instanceof Variable) { | 126 | if (eObject instanceof Variable) { |
112 | // Variables are always private to the containing predicate definition. | 127 | // 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 @@ | |||
6 | package tools.refinery.language.scoping; | 6 | package tools.refinery.language.scoping; |
7 | 7 | ||
8 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
9 | import com.google.inject.name.Named; | ||
9 | import org.eclipse.emf.ecore.EObject; | 10 | import org.eclipse.emf.ecore.EObject; |
10 | import org.eclipse.emf.ecore.EReference; | 11 | import org.eclipse.emf.ecore.EReference; |
11 | import org.eclipse.emf.ecore.resource.Resource; | 12 | import org.eclipse.emf.ecore.resource.Resource; |
@@ -18,13 +19,14 @@ import org.eclipse.xtext.scoping.IScope; | |||
18 | import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeDelegatingScopeProvider; | 19 | import org.eclipse.xtext.scoping.impl.AbstractGlobalScopeDelegatingScopeProvider; |
19 | import org.eclipse.xtext.scoping.impl.SelectableBasedScope; | 20 | import org.eclipse.xtext.scoping.impl.SelectableBasedScope; |
20 | import org.eclipse.xtext.util.IResourceScopeCache; | 21 | import org.eclipse.xtext.util.IResourceScopeCache; |
21 | import tools.refinery.language.naming.NamingUtil; | 22 | import tools.refinery.language.naming.ProblemQualifiedNameProvider; |
22 | 23 | ||
23 | public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScopeProvider { | 24 | public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScopeProvider { |
24 | private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemLocalScopeProvider.CACHE_KEY"; | 25 | private static final String CACHE_KEY = "tools.refinery.language.scoping.ProblemLocalScopeProvider.CACHE_KEY"; |
25 | 26 | ||
26 | @Inject | 27 | @Inject |
27 | private IQualifiedNameProvider qualifiedNameProvider; | 28 | @Named(ProblemQualifiedNameProvider.NAMED_DELEGATE) |
29 | private IQualifiedNameProvider delegateQualifiedNameProvider; | ||
28 | 30 | ||
29 | @Inject | 31 | @Inject |
30 | private IResourceDescriptionsProvider resourceDescriptionsProvider; | 32 | private IResourceDescriptionsProvider resourceDescriptionsProvider; |
@@ -64,7 +66,7 @@ public class ProblemLocalScopeProvider extends AbstractGlobalScopeDelegatingScop | |||
64 | if (rootElement == null) { | 66 | if (rootElement == null) { |
65 | return new LocalImports(resourceDescription, null); | 67 | return new LocalImports(resourceDescription, null); |
66 | } | 68 | } |
67 | var rootName = NamingUtil.stripRootPrefix(qualifiedNameProvider.getFullyQualifiedName(rootElement)); | 69 | var rootName = delegateQualifiedNameProvider.getFullyQualifiedName(rootElement); |
68 | if (rootName == null) { | 70 | if (rootName == null) { |
69 | return new LocalImports(resourceDescription, null); | 71 | return new LocalImports(resourceDescription, null); |
70 | } | 72 | } |
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 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.scoping.imports; | ||
7 | |||
8 | import com.google.common.base.Splitter; | ||
9 | import com.google.common.cache.Cache; | ||
10 | import com.google.common.cache.CacheBuilder; | ||
11 | import org.apache.log4j.Logger; | ||
12 | import org.eclipse.emf.common.notify.Notification; | ||
13 | import org.eclipse.emf.common.notify.impl.AdapterImpl; | ||
14 | import org.eclipse.emf.common.util.URI; | ||
15 | import org.eclipse.emf.ecore.EObject; | ||
16 | import org.eclipse.emf.ecore.resource.Resource; | ||
17 | import org.eclipse.emf.ecore.resource.ResourceSet; | ||
18 | import org.eclipse.emf.ecore.util.EcoreUtil; | ||
19 | import org.eclipse.xtext.naming.QualifiedName; | ||
20 | import tools.refinery.language.library.RefineryLibrary; | ||
21 | |||
22 | import java.io.File; | ||
23 | import java.nio.file.Path; | ||
24 | import java.util.*; | ||
25 | |||
26 | public class ImportAdapter extends AdapterImpl { | ||
27 | private static final Logger LOG = Logger.getLogger(ImportAdapter.class); | ||
28 | private static final List<RefineryLibrary> DEFAULT_LIBRARIES; | ||
29 | private static final List<Path> DEFAULT_PATHS; | ||
30 | |||
31 | static { | ||
32 | var serviceLoader = ServiceLoader.load(RefineryLibrary.class); | ||
33 | var defaultLibraries = new ArrayList<RefineryLibrary>(); | ||
34 | for (var service : serviceLoader) { | ||
35 | defaultLibraries.add(service); | ||
36 | } | ||
37 | DEFAULT_LIBRARIES = List.copyOf(defaultLibraries); | ||
38 | var pathEnv = System.getenv("REFINERY_LIBRARY_PATH"); | ||
39 | if (pathEnv == null) { | ||
40 | DEFAULT_PATHS = List.of(); | ||
41 | } else { | ||
42 | DEFAULT_PATHS = Splitter.on(File.pathSeparatorChar) | ||
43 | .splitToStream(pathEnv) | ||
44 | .map(pathString -> Path.of(pathString).toAbsolutePath().normalize()) | ||
45 | .toList(); | ||
46 | } | ||
47 | } | ||
48 | |||
49 | private final List<RefineryLibrary> libraries; | ||
50 | private final List<Path> libraryPaths; | ||
51 | private final Cache<QualifiedName, QualifiedName> failedResolutions = | ||
52 | CacheBuilder.newBuilder().maximumSize(100).build(); | ||
53 | private final Map<QualifiedName, URI> qualifiedNameToUriMap = new LinkedHashMap<>(); | ||
54 | private final Map<URI, QualifiedName> uriToQualifiedNameMap = new LinkedHashMap<>(); | ||
55 | |||
56 | private ImportAdapter(ResourceSet resourceSet) { | ||
57 | libraries = new ArrayList<>(DEFAULT_LIBRARIES); | ||
58 | libraryPaths = new ArrayList<>(DEFAULT_PATHS); | ||
59 | for (var resource : resourceSet.getResources()) { | ||
60 | resourceAdded(resource); | ||
61 | } | ||
62 | } | ||
63 | |||
64 | @Override | ||
65 | public boolean isAdapterForType(Object type) { | ||
66 | return type == ImportAdapter.class; | ||
67 | } | ||
68 | |||
69 | public List<RefineryLibrary> getLibraries() { | ||
70 | return libraries; | ||
71 | } | ||
72 | |||
73 | public List<Path> getLibraryPaths() { | ||
74 | return libraryPaths; | ||
75 | } | ||
76 | |||
77 | public URI resolveQualifiedName(QualifiedName qualifiedName) { | ||
78 | var uri = getResolvedUri(qualifiedName); | ||
79 | if (uri != null) { | ||
80 | return uri; | ||
81 | } | ||
82 | if (isFailed(qualifiedName)) { | ||
83 | return null; | ||
84 | } | ||
85 | for (var library : libraries) { | ||
86 | var result = library.resolveQualifiedName(qualifiedName, libraryPaths); | ||
87 | if (result.isPresent()) { | ||
88 | uri = result.get(); | ||
89 | markAsResolved(qualifiedName, uri); | ||
90 | return uri; | ||
91 | } | ||
92 | } | ||
93 | markAsUnresolved(qualifiedName); | ||
94 | return null; | ||
95 | } | ||
96 | |||
97 | private URI getResolvedUri(QualifiedName qualifiedName) { | ||
98 | return qualifiedNameToUriMap.get(qualifiedName); | ||
99 | } | ||
100 | |||
101 | private boolean isFailed(QualifiedName qualifiedName) { | ||
102 | return failedResolutions.getIfPresent(qualifiedName) != null; | ||
103 | } | ||
104 | |||
105 | private void markAsResolved(QualifiedName qualifiedName, URI uri) { | ||
106 | if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) { | ||
107 | throw new IllegalArgumentException("Already resolved " + qualifiedName); | ||
108 | } | ||
109 | // We don't need to signal an error here, because modules with multiple qualified names will lead to | ||
110 | // validation errors later. | ||
111 | uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName); | ||
112 | failedResolutions.invalidate(qualifiedName); | ||
113 | } | ||
114 | |||
115 | private void markAsUnresolved(QualifiedName qualifiedName) { | ||
116 | failedResolutions.put(qualifiedName, qualifiedName); | ||
117 | } | ||
118 | |||
119 | public QualifiedName getQualifiedName(URI uri) { | ||
120 | return uriToQualifiedNameMap.get(uri); | ||
121 | } | ||
122 | |||
123 | @Override | ||
124 | public void notifyChanged(Notification msg) { | ||
125 | switch (msg.getEventType()) { | ||
126 | case Notification.ADD -> { | ||
127 | if (msg.getNewValue() instanceof Resource resource) { | ||
128 | resourceAdded(resource); | ||
129 | } | ||
130 | } | ||
131 | case Notification.ADD_MANY -> { | ||
132 | if (msg.getNewValue() instanceof List<?> list) { | ||
133 | manyResourcesAdded(list); | ||
134 | } | ||
135 | } | ||
136 | case Notification.REMOVE -> { | ||
137 | if (msg.getOldValue() instanceof Resource resource) { | ||
138 | resourceRemoved(resource); | ||
139 | } | ||
140 | } | ||
141 | case Notification.REMOVE_MANY -> { | ||
142 | if (msg.getOldValue() instanceof List<?> list) { | ||
143 | manyResourcesRemoved(list); | ||
144 | } | ||
145 | } | ||
146 | default -> { | ||
147 | // Nothing to update. | ||
148 | } | ||
149 | } | ||
150 | } | ||
151 | |||
152 | private void manyResourcesAdded(List<?> list) { | ||
153 | for (var element : list) { | ||
154 | if (element instanceof Resource resource) { | ||
155 | resourceAdded(resource); | ||
156 | } | ||
157 | } | ||
158 | } | ||
159 | |||
160 | private void manyResourcesRemoved(List<?> list) { | ||
161 | for (var element : list) { | ||
162 | if (element instanceof Resource resource) { | ||
163 | resourceRemoved(resource); | ||
164 | } | ||
165 | } | ||
166 | } | ||
167 | |||
168 | private void resourceAdded(Resource resource) { | ||
169 | var uri = resource.getURI(); | ||
170 | for (var library : libraries) { | ||
171 | var result = library.getQualifiedName(uri, libraryPaths); | ||
172 | if (result.isPresent()) { | ||
173 | var qualifiedName = result.get(); | ||
174 | var previousQualifiedName = uriToQualifiedNameMap.putIfAbsent(uri, qualifiedName); | ||
175 | if (previousQualifiedName == null) { | ||
176 | if (qualifiedNameToUriMap.put(qualifiedName, uri) != null) { | ||
177 | throw new IllegalArgumentException("Duplicate resource for" + qualifiedName); | ||
178 | } | ||
179 | } else if (!previousQualifiedName.equals(qualifiedName)) { | ||
180 | LOG.warn("Expected %s to have qualified name %s, got %s instead".formatted( | ||
181 | uri, previousQualifiedName, qualifiedName)); | ||
182 | } | ||
183 | } | ||
184 | } | ||
185 | } | ||
186 | |||
187 | private void resourceRemoved(Resource resource) { | ||
188 | var qualifiedName = uriToQualifiedNameMap.remove(resource.getURI()); | ||
189 | if (qualifiedName != null) { | ||
190 | qualifiedNameToUriMap.remove(qualifiedName); | ||
191 | } | ||
192 | } | ||
193 | |||
194 | public static ImportAdapter getOrInstall(ResourceSet resourceSet) { | ||
195 | var adapter = getAdapter(resourceSet); | ||
196 | if (adapter == null) { | ||
197 | adapter = new ImportAdapter(resourceSet); | ||
198 | resourceSet.eAdapters().add(adapter); | ||
199 | } | ||
200 | return adapter; | ||
201 | } | ||
202 | |||
203 | private static ImportAdapter getAdapter(ResourceSet resourceSet) { | ||
204 | return (ImportAdapter) EcoreUtil.getAdapter(resourceSet.eAdapters(), ImportAdapter.class); | ||
205 | } | ||
206 | |||
207 | public static void copySettings(EObject context, ResourceSet newResourceSet) { | ||
208 | var resource = context.eResource(); | ||
209 | if (resource == null) { | ||
210 | return; | ||
211 | } | ||
212 | var originalResourceSet = resource.getResourceSet(); | ||
213 | if (originalResourceSet == null) { | ||
214 | return; | ||
215 | } | ||
216 | copySettings(originalResourceSet, newResourceSet); | ||
217 | } | ||
218 | |||
219 | public static void copySettings(ResourceSet originalResourceSet, ResourceSet newResourceSet) { | ||
220 | var originalAdapter = getAdapter(originalResourceSet); | ||
221 | if (originalAdapter == null) { | ||
222 | return; | ||
223 | } | ||
224 | var newAdapter = getOrInstall(newResourceSet); | ||
225 | newAdapter.libraries.clear(); | ||
226 | newAdapter.libraries.addAll(originalAdapter.libraries); | ||
227 | newAdapter.libraryPaths.clear(); | ||
228 | newAdapter.libraryPaths.addAll(originalAdapter.libraryPaths); | ||
229 | } | ||
230 | } | ||
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; | |||
18 | import org.eclipse.xtext.nodemodel.util.NodeModelUtils; | 18 | import org.eclipse.xtext.nodemodel.util.NodeModelUtils; |
19 | import org.eclipse.xtext.resource.IEObjectDescription; | 19 | import org.eclipse.xtext.resource.IEObjectDescription; |
20 | import org.eclipse.xtext.util.IResourceScopeCache; | 20 | import org.eclipse.xtext.util.IResourceScopeCache; |
21 | import tools.refinery.language.library.RefineryLibraries; | ||
22 | import tools.refinery.language.model.problem.ImportStatement; | 21 | import tools.refinery.language.model.problem.ImportStatement; |
23 | import tools.refinery.language.model.problem.Problem; | 22 | import tools.refinery.language.model.problem.Problem; |
24 | import tools.refinery.language.model.problem.ProblemPackage; | 23 | import tools.refinery.language.model.problem.ProblemPackage; |
@@ -54,28 +53,49 @@ public class ImportCollector { | |||
54 | if (resource.getContents().isEmpty() || !(resource.getContents().getFirst() instanceof Problem problem)) { | 53 | if (resource.getContents().isEmpty() || !(resource.getContents().getFirst() instanceof Problem problem)) { |
55 | return ImportCollection.EMPTY; | 54 | return ImportCollection.EMPTY; |
56 | } | 55 | } |
56 | var resourceSet = resource.getResourceSet(); | ||
57 | if (resourceSet == null) { | ||
58 | return ImportCollection.EMPTY; | ||
59 | } | ||
57 | Map<QualifiedName, Set<QualifiedName>> aliasesMap = new LinkedHashMap<>(); | 60 | Map<QualifiedName, Set<QualifiedName>> aliasesMap = new LinkedHashMap<>(); |
58 | for (var statement : problem.getStatements()) { | 61 | for (var statement : problem.getStatements()) { |
59 | if (statement instanceof ImportStatement importStatement) { | 62 | if (statement instanceof ImportStatement importStatement) { |
60 | collectImportStatement(importStatement, aliasesMap); | 63 | collectImportStatement(importStatement, aliasesMap); |
61 | } | 64 | } |
62 | } | 65 | } |
66 | var adapter = ImportAdapter.getOrInstall(resourceSet); | ||
63 | var collection = new ImportCollection(); | 67 | var collection = new ImportCollection(); |
64 | collection.addAll(RefineryLibraries.getAutomaticImports()); | 68 | collectAutomaticImports(collection, adapter); |
69 | collectExplicitImports(aliasesMap, collection, adapter); | ||
70 | collection.remove(resource.getURI()); | ||
71 | return collection; | ||
72 | } | ||
73 | |||
74 | private void collectAutomaticImports(ImportCollection importCollection, ImportAdapter adapter) { | ||
75 | for (var library : adapter.getLibraries()) { | ||
76 | for (var qualifiedName : library.getAutomaticImports()) { | ||
77 | var uri = adapter.resolveQualifiedName(qualifiedName); | ||
78 | if (uri != null) { | ||
79 | importCollection.add(NamedImport.implicit(uri, qualifiedName)); | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | } | ||
84 | |||
85 | private void collectExplicitImports(Map<QualifiedName, Set<QualifiedName>> aliasesMap, | ||
86 | ImportCollection collection, ImportAdapter adapter) { | ||
65 | for (var entry : aliasesMap.entrySet()) { | 87 | for (var entry : aliasesMap.entrySet()) { |
66 | var qualifiedName = entry.getKey(); | 88 | var qualifiedName = entry.getKey(); |
67 | RefineryLibraries.resolveQualifiedName(qualifiedName).ifPresent(uri -> { | 89 | var uri = adapter.resolveQualifiedName(qualifiedName); |
68 | if (!uri.equals(resource.getURI())) { | 90 | if (uri != null) { |
69 | var aliases = entry.getValue(); | 91 | var aliases = entry.getValue(); |
70 | collection.add(NamedImport.explicit(uri, qualifiedName, List.copyOf(aliases))); | 92 | collection.add(NamedImport.explicit(uri, qualifiedName, List.copyOf(aliases))); |
71 | } | 93 | } |
72 | }); | ||
73 | } | 94 | } |
74 | collection.remove(resource.getURI()); | ||
75 | return collection; | ||
76 | } | 95 | } |
77 | 96 | ||
78 | private void collectImportStatement(ImportStatement importStatement, Map<QualifiedName, Set<QualifiedName>> aliasesMap) { | 97 | private void collectImportStatement(ImportStatement importStatement, |
98 | Map<QualifiedName, Set<QualifiedName>> aliasesMap) { | ||
79 | var nodes = NodeModelUtils.findNodesForFeature(importStatement, | 99 | var nodes = NodeModelUtils.findNodesForFeature(importStatement, |
80 | ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE); | 100 | ProblemPackage.Literals.IMPORT_STATEMENT__IMPORTED_MODULE); |
81 | var aliasString = importStatement.getAlias(); | 101 | 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 { | |||
124 | }; | 124 | }; |
125 | } | 125 | } |
126 | 126 | ||
127 | public static boolean isModule(Problem problem) { | ||
128 | return problem.getKind() == ModuleKind.MODULE; | ||
129 | } | ||
130 | |||
127 | public static boolean isInModule(EObject eObject) { | 131 | public static boolean isInModule(EObject eObject) { |
128 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); | 132 | var problem = EcoreUtil2.getContainerOfType(eObject, Problem.class); |
129 | return problem != null && problem.getKind() == ModuleKind.MODULE; | 133 | return problem != null && isModule(problem); |
130 | } | 134 | } |
131 | } | 135 | } |
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 @@ | |||
2 | # | 2 | # |
3 | # SPDX-License-Identifier: EPL-2.0 | 3 | # SPDX-License-Identifier: EPL-2.0 |
4 | tools.refinery.language.library.BuiltinLibrary | 4 | tools.refinery.language.library.BuiltinLibrary |
5 | 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 @@ | |||
1 | % SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | 1 | % SPDX-FileCopyrightText: 2021-2024 The Refinery Authors <https://refinery.tools/> |
2 | % | 2 | % |
3 | % SPDX-License-Identifier: EPL-2.0 | 3 | % SPDX-License-Identifier: EPL-2.0 |
4 | problem builtin. | 4 | module builtin. |
5 | 5 | ||
6 | abstract class node. | 6 | abstract class node. |
7 | 7 | ||