From fd9cb9acb49a8dd08ec8fd5f86f5ca54c0e913b7 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 28 Sep 2026 14:25:10 +0200 Subject: [PATCH 01/17] Remove unused field in `URIResolverRegistry` --- src/org/rascalmpl/uri/URIResolverRegistry.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 79b5468115d..7c33dce40a8 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -58,7 +58,6 @@ import io.usethesource.vallang.type.TypeStore; public class URIResolverRegistry { - private static final int FILE_BUFFER_SIZE = 8 * 1024; private static final String RESOLVERS_CONFIG = "org/rascalmpl/uri/resolvers.config"; private static final IValueFactory vf = ValueFactoryFactory.getValueFactory(); private final Map inputResolvers = new ConcurrentHashMap<>(); From 6c3d7ba4b5edeb14fff9fcb18914eb451bd0ce11 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 28 Sep 2026 14:27:43 +0200 Subject: [PATCH 02/17] Add extra null check to avoid NPE --- src/org/rascalmpl/uri/URIResolverRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 7c33dce40a8..f86705a4e3c 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -334,7 +334,7 @@ private static ISourceLocation resolveAndFixOffsets(ISourceLocation loc, ILogica loc = resolver.resolve(loc); } - if (loc == null && prev.hasOffsetLength()) { + if (loc == null && resolver != null && prev.hasOffsetLength()) { loc = resolver.resolve(URIUtil.removeOffset(prev)); removedOffset = true; } From f8ba778545f2cd6c5effeb8b41cb9da5009bdfb4 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 28 Sep 2026 14:35:54 +0200 Subject: [PATCH 03/17] Add helper function `tryResolve`. The new `tryResolve` is the same as the existing `safeResolve`, except `tryResolve` propagates both `IOException`s and unchecked exceptions to the caller, whereas `safeResolve` catches and ignores all exceptions. --- .../rascalmpl/uri/URIResolverRegistry.java | 57 ++++++++++++------- .../uri/UnsupportedSchemeException.java | 4 ++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index f86705a4e3c..7169042e69d 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -393,27 +393,46 @@ else if (loc.hasOffsetLength()) { // the logical location offsets relative to th return loc; } - private ISourceLocation physicalLocation(ISourceLocation loc) throws IOException { - ISourceLocation original = loc; - while (loc != null && logicalResolvers.containsKey(loc.getScheme())) { - Map map = logicalResolvers.get(loc.getScheme()); - String auth = loc.hasAuthority() ? loc.getAuthority() : ""; - ILogicalSourceLocationResolver resolver = map.get(auth); - loc = resolveAndFixOffsets(loc, resolver, map.values()); - } - - if (externalRegistry != null && original != null) { - try { - var externalResolve = loc == null ? original : loc; - if (externalRegistry.supportsLogical(externalResolve.getScheme())) { - var externalResult = resolveAndFixOffsets(externalResolve, externalRegistry, Collections.emptyList()); - return externalResult == null ? loc : externalResult; - } - } catch (IOException e) { - // Ignore remote IO errors + /** + * Computes the physical location for the provided location {@code loc}, according to the following rules: + *
    + *
  • If {@code loc} is logical, and a resolver does exists for its scheme/authority, and it can be resolved, then return that physical location. + *
  • If {@code loc} is logical, and a resolver does exists for its scheme/authority, but it cannot be resolved, then return null. + *
  • If {@code loc} is logical, but a resolver doesn't exist for its scheme/authority, then throw an exception. + *
  • If {@code loc} is physical, then return {@code loc}. + *
+ */ + private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IOException { + var scheme = loc.getScheme(); + + // Case: `loc` is a logical location with a local resolver + if (logicalResolvers.containsKey(scheme)) { + var auth = loc.getAuthority(); + var resolversByAuth = logicalResolvers.getOrDefault(scheme, Collections.emptyMap()); + var resolver = resolversByAuth.get(auth); + if (resolver == null) { + throw new UnsupportedSchemeException(scheme, auth); } + + var resolved = resolveAndFixOffsets(loc, resolver, resolversByAuth.values()); + return resolved == null ? null : physicalLocation(resolved); } - return loc; + + // Case: `loc` is a logical location with a remote resolver + else if (externalRegistry != null && externalRegistry.supportsLogical(scheme)) { + var resolved = resolveAndFixOffsets(loc, externalRegistry, Collections.emptyList()); + return resolved == null ? null : physicalLocation(resolved); + } + + // Case: `loc` is a physical location + else { + return loc; + } + } + + private @NonNull ISourceLocation tryResolve(@NonNull ISourceLocation loc) throws IOException { + var resolved = physicalLocation(loc); + return resolved != null ? resolved : loc; } private @NonNull ISourceLocation safeResolve(@NonNull ISourceLocation loc) { diff --git a/src/org/rascalmpl/uri/UnsupportedSchemeException.java b/src/org/rascalmpl/uri/UnsupportedSchemeException.java index a4695ac808c..6887440fafe 100644 --- a/src/org/rascalmpl/uri/UnsupportedSchemeException.java +++ b/src/org/rascalmpl/uri/UnsupportedSchemeException.java @@ -19,4 +19,8 @@ public class UnsupportedSchemeException extends IOException { public UnsupportedSchemeException(String scheme) { super("Unsupported scheme '" + scheme + "'"); } + + public UnsupportedSchemeException(String scheme, String authority) { + super("Unsupported scheme '" + scheme + "' and authority '" + authority + "'"); + } } From 046db25a861ff14d0b913a8eeb0bf6cec6555144 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 28 Sep 2026 14:36:03 +0200 Subject: [PATCH 04/17] Replace `safeResolve` with `tryResolve` in each method whose signature *already* allows `IOException`s to be thrown (i.e., callers of those methods should already be sufficiently resilient) --- .../rascalmpl/uri/URIResolverRegistry.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 7169042e69d..ffe3061e6bb 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -584,7 +584,7 @@ public boolean exists(ISourceLocation uri) { * @throws IOException */ public void setLastModified(ISourceLocation uri, long timestamp) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); @@ -606,7 +606,7 @@ public boolean isDirectory(ISourceLocation uri) { } public void mkDirectory(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); if (resolver == null) { @@ -624,7 +624,7 @@ private void notifyWatcher(ISourceLocation key, ISourceLocationChanged event) { } public void remove(ISourceLocation uri, boolean recursive) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput out = getOutputResolver(uri.getScheme()); if (out == null) { @@ -658,8 +658,8 @@ else if (listEntries(uri).length != 0) { * exists and overwrite was `false`. */ public void rename(ISourceLocation from, ISourceLocation to, boolean overwrite) throws IOException { - from = safeResolve(from); - to = safeResolve(to); + from = tryResolve(from); + to = tryResolve(to); if (from.getScheme().equals(to.getScheme())) { ISourceLocationOutput out = getOutputResolver(from.getScheme()); @@ -687,7 +687,7 @@ public boolean isFile(ISourceLocation uri) { } public long lastModified(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -706,7 +706,7 @@ public long lastModified(ISourceLocation uri) throws IOException { } public long created(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -725,7 +725,7 @@ public long created(ISourceLocation uri) throws IOException { } public boolean isWritable(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); var resolver = getOutputResolver(uri.getScheme()); if (resolver != null) { return resolver.isWritable(uri); @@ -737,7 +737,7 @@ public boolean isWritable(ISourceLocation uri) throws IOException { return false; } public boolean isReadable(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); var resolver = getInputResolver(uri.getScheme()); if (resolver == null) { throw new UnsupportedSchemeException(uri.getScheme()); @@ -752,7 +752,7 @@ public boolean isReadable(ISourceLocation uri) throws IOException { * @throws IOException */ public long size(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -767,7 +767,8 @@ private boolean isRootLogical(ISourceLocation uri) { && logicalResolvers.containsKey(uri.getScheme()); } - public String[] listEntries(ISourceLocation uri) throws IOException { uri = safeResolve(uri); + public String[] listEntries(ISourceLocation uri) throws IOException { + uri = tryResolve(uri); if (isRootLogical(uri)) { // if it's a location without any path and authority // we want to list possible authorities if it's a logical one @@ -801,8 +802,8 @@ private boolean isRootLogical(ISourceLocation uri) { * when a source folder or file can not be read */ public void copy(ISourceLocation source, ISourceLocation target, boolean recursive, boolean overwrite) throws IOException { - var sourceResolved = safeResolve(source); - var targetResolved = safeResolve(target); + var sourceResolved = tryResolve(source); + var targetResolved = tryResolve(target); if (sourceResolved.getScheme().equals(targetResolved.getScheme())) { var commonResolver = getOutputResolver(sourceResolved.getScheme()); if (commonResolver != null && commonResolver.supportsCopy()) { @@ -893,7 +894,7 @@ public Reader getCharacterReader(ISourceLocation uri, String encoding) throws IO } public Reader getCharacterReader(ISourceLocation uri, Charset encoding) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); Reader res = new UnicodeInputStreamReader(getInputStream(uri), encoding); if (uri.hasOffsetLength()) { @@ -914,12 +915,12 @@ public Reader getCharacterReader(ISourceLocation uri, Charset encoding) throws I * @throws IOException */ public Writer getCharacterWriter(ISourceLocation uri, String encoding, boolean append) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); return new UnicodeOutputStreamWriter(getOutputStream(uri, append), encoding); } public ClassLoader getClassLoader(ISourceLocation uri, ClassLoader parent) throws IOException { - IClassloaderLocationResolver resolver = getClassloaderResolver(safeResolve(uri).getScheme()); + IClassloaderLocationResolver resolver = getClassloaderResolver(tryResolve(uri).getScheme()); if (resolver != null) { // we always try the most specific implementation for efficiency's sake @@ -973,7 +974,7 @@ protected Class findClass(final String qualifiedClassName) throws ClassNotFou public InputStream getInputStream(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null) { @@ -984,7 +985,7 @@ public InputStream getInputStream(ISourceLocation uri) throws IOException { } public FileChannel getReadableFileChannel(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null || !resolver.supportsReadableFileChannel()) { @@ -1017,7 +1018,7 @@ public Charset detectCharset(ISourceLocation sloc) { } public Charset getCharset(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationInput resolver = getInputResolver(uri.getScheme()); if (resolver == null || (externalRegistry != null && resolver == externalRegistry && !externalRegistry.supportsGetCharset(uri.getScheme()))) { @@ -1028,7 +1029,7 @@ public Charset getCharset(ISourceLocation uri) throws IOException { } public OutputStream getOutputStream(ISourceLocation uri, boolean append) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); boolean existedBefore = exists(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); @@ -1046,7 +1047,7 @@ public OutputStream getOutputStream(ISourceLocation uri, boolean append) throws } public FileChannel getWriteableFileChannel(ISourceLocation uri, boolean append) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocationOutput resolver = getOutputResolver(uri.getScheme()); if (resolver == null || !resolver.supportsWritableFileChannel()) { @@ -1066,7 +1067,7 @@ public FileChannel getWriteableFileChannel(ISourceLocation uri, boolean append) } private void mkParentDir(ISourceLocation uri) throws IOException { - uri = safeResolve(uri); + uri = tryResolve(uri); ISourceLocation parentURI = URIUtil.getParentLocation(uri); if (parentURI != null && !parentURI.equals(uri) && !exists(parentURI)) { @@ -1155,7 +1156,7 @@ public boolean hasNativelyWatchableResolver(ISourceLocation loc) { } public FileAttributes stat(ISourceLocation loc) throws IOException { - loc = safeResolve(loc); + loc = tryResolve(loc); var resolver = getInputResolver(loc.getScheme()); if (resolver == null) { throw new IOException("Unsupported scheme: " + loc.getScheme()); From 64c2663c7c5109d5d6e66e83e43b4b0d37543979 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Wed, 30 Sep 2026 11:02:36 +0200 Subject: [PATCH 05/17] Add `UnsupportedAuthorityException` --- .../uri/UnsupportedAuthorityException.java | 37 +++++++++++++++++++ .../uri/UnsupportedSchemeException.java | 4 -- 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/org/rascalmpl/uri/UnsupportedAuthorityException.java diff --git a/src/org/rascalmpl/uri/UnsupportedAuthorityException.java b/src/org/rascalmpl/uri/UnsupportedAuthorityException.java new file mode 100644 index 00000000000..1053bff1836 --- /dev/null +++ b/src/org/rascalmpl/uri/UnsupportedAuthorityException.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018-2026, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.uri; + +import java.io.IOException; + +public class UnsupportedAuthorityException extends IOException { + private static final long serialVersionUID = -3261531203667050736L; + + public UnsupportedAuthorityException(String scheme, String authority) { + super("Unsupported authority '" + authority + "' for scheme '\" + scheme + \"'"); + } +} diff --git a/src/org/rascalmpl/uri/UnsupportedSchemeException.java b/src/org/rascalmpl/uri/UnsupportedSchemeException.java index 6887440fafe..a4695ac808c 100644 --- a/src/org/rascalmpl/uri/UnsupportedSchemeException.java +++ b/src/org/rascalmpl/uri/UnsupportedSchemeException.java @@ -19,8 +19,4 @@ public class UnsupportedSchemeException extends IOException { public UnsupportedSchemeException(String scheme) { super("Unsupported scheme '" + scheme + "'"); } - - public UnsupportedSchemeException(String scheme, String authority) { - super("Unsupported scheme '" + scheme + "' and authority '" + authority + "'"); - } } From 57a8b791a9030c6779145109943cd71ab63bbc72 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Wed, 30 Sep 2026 11:09:16 +0200 Subject: [PATCH 06/17] Update `physicalLocation` to behave as stated in the JavaDoc (fix a few issues) --- .../rascalmpl/uri/URIResolverRegistry.java | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index ffe3061e6bb..0ad57c43069 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -326,7 +326,7 @@ public ISourceLocation logicalToPhysical(ISourceLocation loc) throws IOException return result; } - private static ISourceLocation resolveAndFixOffsets(ISourceLocation loc, ILogicalSourceLocationResolver resolver, Iterable backups) throws IOException { + private static ISourceLocation resolveAndFixOffsets(ISourceLocation loc, ILogicalSourceLocationResolver resolver) throws IOException { ISourceLocation prev = loc; boolean removedOffset = false; @@ -339,22 +339,6 @@ private static ISourceLocation resolveAndFixOffsets(ISourceLocation loc, ILogica removedOffset = true; } - if (loc == null || prev.equals(loc)) { - for (ILogicalSourceLocationResolver backup : backups) { - removedOffset = false; - loc = backup.resolve(prev); - - if (loc == null && prev.hasOffsetLength()) { - loc = backup.resolve(URIUtil.removeOffset(prev)); - removedOffset = true; - } - - if (loc != null && !prev.equals(loc)) { - break; // continue to offset/length handling below with found location - } - } - } - if (loc == null || prev.equals(loc)) { return null; } @@ -396,35 +380,40 @@ else if (loc.hasOffsetLength()) { // the logical location offsets relative to th /** * Computes the physical location for the provided location {@code loc}, according to the following rules: *
    - *
  • If {@code loc} is logical, and a resolver does exists for its scheme/authority, and it can be resolved, then return that physical location. - *
  • If {@code loc} is logical, and a resolver does exists for its scheme/authority, but it cannot be resolved, then return null. - *
  • If {@code loc} is logical, but a resolver doesn't exist for its scheme/authority, then throw an exception. - *
  • If {@code loc} is physical, then return {@code loc}. + *
  • If {@code loc} is known to be logical, and a resolver does exists for its authority, and it can be resolved, then return that physical location. + *
  • If {@code loc} is known to be logical, and a resolver does exists for its authority, but it cannot be resolved, then return null. + *
  • If {@code loc} is known to be logical, but a resolver doesn't exist for its authority, then throw an exception. + *
  • If {@code loc} is unknown to be logical, then return {@code loc}. *
*/ private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IOException { var scheme = loc.getScheme(); - // Case: `loc` is a logical location with a local resolver + // Case: `loc` is known to be logical and has a local resolver if (logicalResolvers.containsKey(scheme)) { var auth = loc.getAuthority(); - var resolversByAuth = logicalResolvers.getOrDefault(scheme, Collections.emptyMap()); - var resolver = resolversByAuth.get(auth); + var resolversByAuth = logicalResolvers.getOrDefault(scheme, Collections.emptyMap()); + + var resolver = resolversByAuth.getOrDefault(auth, resolversByAuth.get("")); if (resolver == null) { - throw new UnsupportedSchemeException(scheme, auth); + throw new UnsupportedAuthorityException(scheme, auth); + } + + var resolved = resolveAndFixOffsets(loc, resolver); + if (resolved == null && resolver != resolversByAuth.get(auth)) { // Tried default resolver, but it failed + throw new UnsupportedAuthorityException(scheme, auth); } - var resolved = resolveAndFixOffsets(loc, resolver, resolversByAuth.values()); - return resolved == null ? null : physicalLocation(resolved); + return resolved != null ? physicalLocation(resolved) : null; } - // Case: `loc` is a logical location with a remote resolver + // Case: `loc` is known to be logical and has a remote resolver else if (externalRegistry != null && externalRegistry.supportsLogical(scheme)) { - var resolved = resolveAndFixOffsets(loc, externalRegistry, Collections.emptyList()); - return resolved == null ? null : physicalLocation(resolved); + var resolved = resolveAndFixOffsets(loc, externalRegistry); + return resolved != null ? physicalLocation(resolved) : null; } - // Case: `loc` is a physical location + // Case: `loc` is unknown to be logical else { return loc; } From 405c30b51f1f47387ab3c5129a79e2ac34c5e5fe Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Wed, 30 Sep 2026 11:13:12 +0200 Subject: [PATCH 07/17] Add test for resolution of logical locations --- .../library/lang/rascal/tests/library/IO.rsc | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc index b9e50751ab1..dff2a9e299f 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc @@ -4,6 +4,72 @@ import IO; import DateTime; import String; +test bool testLogicalLocationResolution() { + str scheme = "test"; + + value exceptionOf(value() f) { + try { + f(); + return ""; + } catch e: { + return e; + } + } + + bool throwsUnsupportedAuthority(value() f) { + return /Unsupported authority/ := ""; + } + + bool throwsExceptionDownstream(value() f) { + return str s := "" && "" != s && /Unsupported authority/ !:= s; + } + + try { + // Register authorities `foo` and `bar` + registerLocations(scheme, "foo", (|://foo/|: |file:///|)); + registerLocations(scheme, "bar", (|://bar/|: |://foo/|)); + + assert lastModified(|://foo/|) == lastModified(|file:///|); + assert lastModified(|://bar/|) == lastModified(|file:///|); + + assert throwsExceptionDownstream(value() { lastModified(|://foo/x/y/z|); }); + assert throwsExceptionDownstream(value() { lastModified(|://bar/x/y/z|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|://baz/|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|://baz/x/y/z|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|://qux/|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|://qux/x/y/z|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|:///|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|:///x/y/z|); }); + + // Register default authority + registerLocations(scheme, "", ( + |://baz/|: |://bar/|, + |://baz/x/y/z|: |file:///x/y/z| + )); + + assert lastModified(|://foo/|) == lastModified(|file:///|); + assert lastModified(|://bar/|) == lastModified(|file:///|); + assert lastModified(|://baz/|) == lastModified(|file:///|); + + assert throwsExceptionDownstream(value() { lastModified(|://foo/x/y/z|); }); + assert throwsExceptionDownstream(value() { lastModified(|://bar/x/y/z|); }); + assert throwsExceptionDownstream(value() { lastModified(|://baz/x/y/z|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|://qux/|); }); + assert throwsUnsupportedAuthority(value() { lastModified(|://qux/x/y/z|); }); + assert throwsExceptionDownstream(value() { lastModified(|:///|); }); + assert throwsExceptionDownstream(value() { lastModified(|:///x/y/z|); }); + + return true; + } + catch false: // Catch block only to make finally block grammatical + throw false; + finally { + unregisterLocations(scheme, "foo"); + unregisterLocations(scheme, "bar"); + unregisterLocations(scheme, ""); + } +} + test bool testFileCopyCompletely() { writeFile(|tmp:///longFile|, "123456789"); writeFile(|tmp:///shortFile|, "321"); From ce32707365bf51a3d52b7ee55bb275477a814c5a Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Wed, 30 Sep 2026 11:34:11 +0200 Subject: [PATCH 08/17] Add documentation to `ILogicalSourceLocationResolver` --- .../uri/ILogicalSourceLocationResolver.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java index 05f17c91303..a7f377dcc56 100644 --- a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java +++ b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java @@ -4,8 +4,22 @@ import io.usethesource.vallang.ISourceLocation; +/** + * Resolver of logical locations with a certain {@link #scheme} and {@link #authority}. Implementations of this + * interface for non-empty authorities are authority-specific resolvers. Implementations for the distinguished + * empty authority are default resolvers. To resolve a logical location, first, an authority-specific resolver + * for the scheme is sought. If it doesn't exist, then the default resolver for the scheme is sought instead. If it + * doesn't exist either, then an exception is thrown. Note: If an authority-specific resolver does exist, but fails, + * then the default resolver will not be sought instead (i.e., the default resolver resembles the default case of a + * switch statement with non-fallthrough cases). + */ public interface ILogicalSourceLocationResolver { ISourceLocation resolve(ISourceLocation input) throws IOException; String scheme(); + + /** + * @return A non-empty string if this is an authority-specific resolver. The empty string if this is a default + * resolver. + */ String authority(); } From a3f26070c33422ce764f8c3895d5659c65bd6919 Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:29:36 +0200 Subject: [PATCH 09/17] Update test scheme --- .../rascalmpl/library/lang/rascal/tests/library/IO.rsc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc index dff2a9e299f..83b3b3e0886 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc @@ -5,7 +5,7 @@ import DateTime; import String; test bool testLogicalLocationResolution() { - str scheme = "test"; + str scheme = "test-logical"; value exceptionOf(value() f) { try { @@ -60,7 +60,7 @@ test bool testLogicalLocationResolution() { assert throwsExceptionDownstream(value() { lastModified(|:///x/y/z|); }); return true; - } + } catch false: // Catch block only to make finally block grammatical throw false; finally { @@ -88,9 +88,9 @@ test bool testFileCopyRecursive() { test bool watchDoesNotCrashOnURIRewrites() { writeFile(|tmp:///watchDoesNotCrashOnURIRewrites/someFile.txt|, "123456789"); - watch(|tmp:///watchDoesNotCrashOnURIRewrites|, true, void (FileSystemChange event) { + watch(|tmp:///watchDoesNotCrashOnURIRewrites|, true, void (FileSystemChange event) { // this should trigger the failing test finally - remove(event.file); + remove(event.file); }); return true; } @@ -141,4 +141,4 @@ test bool renameDirectory() { writeFile(|tmp:///Folder/hello.txt|, "Hello World!"); rename(|tmp:///Folder|, |tmp:///RenamedFolder|); return readFile(|tmp:///RenamedFolder/hello.txt|) == "Hello World!"; -} \ No newline at end of file +} From 1ebdd13f8a0472522452b3626bea041ba46afb12 Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:35:12 +0200 Subject: [PATCH 10/17] Add messages to assertions in tests --- .../library/lang/rascal/tests/library/IO.rsc | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc index 83b3b3e0886..42007e8a351 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc @@ -29,17 +29,18 @@ test bool testLogicalLocationResolution() { registerLocations(scheme, "foo", (|://foo/|: |file:///|)); registerLocations(scheme, "bar", (|://bar/|: |://foo/|)); - assert lastModified(|://foo/|) == lastModified(|file:///|); - assert lastModified(|://bar/|) == lastModified(|file:///|); - - assert throwsExceptionDownstream(value() { lastModified(|://foo/x/y/z|); }); - assert throwsExceptionDownstream(value() { lastModified(|://bar/x/y/z|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|://baz/|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|://baz/x/y/z|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|://qux/|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|://qux/x/y/z|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|:///|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|:///x/y/z|); }); + assert lastModified(|://foo/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + assert lastModified(|://bar/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + + str cause = "Resolution either unexpectedly succeeded or failed with an unexpected exception"; + assert throwsExceptionDownstream(value() { return lastModified(|://foo/x/y/z|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|://bar/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://baz/|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://baz/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///x/y/z|); }) : cause; // Register default authority registerLocations(scheme, "", ( @@ -47,17 +48,17 @@ test bool testLogicalLocationResolution() { |://baz/x/y/z|: |file:///x/y/z| )); - assert lastModified(|://foo/|) == lastModified(|file:///|); - assert lastModified(|://bar/|) == lastModified(|file:///|); - assert lastModified(|://baz/|) == lastModified(|file:///|); - - assert throwsExceptionDownstream(value() { lastModified(|://foo/x/y/z|); }); - assert throwsExceptionDownstream(value() { lastModified(|://bar/x/y/z|); }); - assert throwsExceptionDownstream(value() { lastModified(|://baz/x/y/z|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|://qux/|); }); - assert throwsUnsupportedAuthority(value() { lastModified(|://qux/x/y/z|); }); - assert throwsExceptionDownstream(value() { lastModified(|:///|); }); - assert throwsExceptionDownstream(value() { lastModified(|:///x/y/z|); }); + assert lastModified(|://foo/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + assert lastModified(|://bar/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + assert lastModified(|://baz/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; + + assert throwsExceptionDownstream(value() { return lastModified(|://foo/x/y/z|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|://bar/x/y/z|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|://baz/x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/x/y/z|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|:///|); }) : cause; + assert throwsExceptionDownstream(value() { return lastModified(|:///x/y/z|); }) : cause; return true; } From bb5366c1faf7e7e93d039188490fcc66e8c74402 Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:38:48 +0200 Subject: [PATCH 11/17] Update documentation of `ILogicalSourceLocationResolver` --- .../rascalmpl/uri/ILogicalSourceLocationResolver.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java index a7f377dcc56..dd807c50b32 100644 --- a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java +++ b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java @@ -2,6 +2,8 @@ import java.io.IOException; +import org.checkerframework.checker.nullness.qual.Nullable; + import io.usethesource.vallang.ISourceLocation; /** @@ -14,7 +16,13 @@ * switch statement with non-fallthrough cases). */ public interface ILogicalSourceLocationResolver { - ISourceLocation resolve(ISourceLocation input) throws IOException; + + /** + * @return A non-null location if this is an authority-specific resolver. Possibly null only if this is a default + * resolver and a default for {@code input} isn't available. + */ + @Nullable ISourceLocation resolve(ISourceLocation input) throws IOException; + String scheme(); /** From 5b034e6a304426c1474adf58bd23f2d89bfd038f Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:40:07 +0200 Subject: [PATCH 12/17] Remove else-if/else --- .../rascalmpl/uri/URIResolverRegistry.java | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 0ad57c43069..ce500fdee03 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -4,7 +4,7 @@ * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: - * + * * * Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI * Paul Klint - Paul.Klint@cwi.nl - CWI * Mark Hills * - Mark.Hills@cwi.nl (CWI) * Arnold Lankamp - Arnold.Lankamp@cwi.nl *******************************************************************************/ @@ -80,7 +80,7 @@ private URIResolverRegistry() { /** * Use with care! This (expensive) reinitialization method clears all caches of all resolvers by * reloading them from scratch. - * + * *

* This can be beneficial if the state of a system changes outside of the scope of the resolvers * themselves, for example when projects open or close inside a workspace or when plugins are loaded @@ -89,7 +89,7 @@ private URIResolverRegistry() { * from scratch. If such a URI re-defining event is detected, host environments (IDEs, app * containers, language servers) should call this method. *

- * + * *

* CAVEAT: after this reinitialization all location caches have been removed and so the first * locations to be used may require expensive indexing and probing operations, for example @@ -275,14 +275,14 @@ private OutputStream makeBuffered(ISourceLocation loc, boolean existed, OutputSt if (original instanceof BufferedOutputStream || original instanceof ByteArrayOutputStream) { return new NotifyingOutputStream( - original, - loc, + original, + loc, existed ? ISourceLocationWatcher.modified(loc) : ISourceLocationWatcher.created(loc) ); } - return new NotifyingOutputStream(new BufferedOutputStream(original), - loc, + return new NotifyingOutputStream(new BufferedOutputStream(original), + loc, existed ? ISourceLocationWatcher.modified(loc) : ISourceLocationWatcher.created(loc) ); } @@ -313,7 +313,7 @@ public void write(byte[] b, int off, int len) throws IOException { * this mapping the registered {@link ILogicalSourceLocationResolver} collection is used. These are * indexed first by scheme and then by authority. If the scheme is registered but the authority is * not, then the same lookup is tried again without authority. - * + * * @param loc logical source location * @return physical source location * @throws IOException when there is no registered resolver for the logical scheme provided @@ -392,7 +392,7 @@ private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IO // Case: `loc` is known to be logical and has a local resolver if (logicalResolvers.containsKey(scheme)) { var auth = loc.getAuthority(); - var resolversByAuth = logicalResolvers.getOrDefault(scheme, Collections.emptyMap()); + var resolversByAuth = logicalResolvers.getOrDefault(scheme, Collections.emptyMap()); var resolver = resolversByAuth.getOrDefault(auth, resolversByAuth.get("")); if (resolver == null) { @@ -408,15 +408,13 @@ private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IO } // Case: `loc` is known to be logical and has a remote resolver - else if (externalRegistry != null && externalRegistry.supportsLogical(scheme)) { + if (externalRegistry != null && externalRegistry.supportsLogical(scheme)) { var resolved = resolveAndFixOffsets(loc, externalRegistry); return resolved != null ? physicalLocation(resolved) : null; } // Case: `loc` is unknown to be logical - else { - return loc; - } + return loc; } private @NonNull ISourceLocation tryResolve(@NonNull ISourceLocation loc) throws IOException { @@ -568,7 +566,7 @@ public boolean exists(ISourceLocation uri) { /** * set the last modification date of a file - * + * * @param timestamp in millis since the epoch * @throws IOException */ @@ -639,7 +637,7 @@ else if (listEntries(uri).length != 0) { /** * Moves a file from source name to target name. If the source is a folder, then it is moved recursively. - * + * * @param from existing name of file or folder * @param to new name of file or folder * @param overwrite if `false` and the target folder or file already exists, throw an exception @@ -735,7 +733,7 @@ public boolean isReadable(ISourceLocation uri) throws IOException { } /** - * This is byte size, and should not be exposed to the rascal users. + * This is byte size, and should not be exposed to the rascal users. * @param uri * @return * @throws IOException @@ -812,7 +810,7 @@ public void copy(ISourceLocation source, ISourceLocation target, boolean recursi throw new IOException("can not make directory because file exists: " + target); } } - + mkDirectory(targetResolved); for (String elem : URIResolverRegistry.getInstance().listEntries(sourceResolved)) { @@ -838,7 +836,7 @@ private void copyFile(ISourceLocation source, ISourceLocation target, boolean ov if (exists(target) && overwrite) { remove(target, false); } - + if (supportsReadableFileChannel(source) && supportsWritableFileChannel(target) && size(source) > 8*1024) { try (FileChannel from = getReadableFileChannel(source)) { try (FileChannel to = getWriteableFileChannel(target, false)) { @@ -896,12 +894,12 @@ public Reader getCharacterReader(ISourceLocation uri, Charset encoding) throws I /** * Return a character Writer for the given uri, using the given character encoding. - * + * * @param uri file to write to or append to * @param encoding how to encode individual characters @see Charset * @param append whether to append or start at the beginning. * @return - * @throws IOException + * @throws IOException */ public Writer getCharacterWriter(ISourceLocation uri, String encoding, boolean append) throws IOException { uri = tryResolve(uri); @@ -986,7 +984,7 @@ public FileChannel getReadableFileChannel(ISourceLocation uri) throws IOExceptio public Charset detectCharset(ISourceLocation sloc) { URIResolverRegistry reg = URIResolverRegistry.getInstance(); - + // in case the file already has a encoding, we have to correctly append that. Charset detected = null; try (InputStream in = reg.getInputStream(sloc);) { @@ -1000,8 +998,8 @@ public Charset detectCharset(ISourceLocation sloc) { // we stick with the default if something happened above. // if the writing hereafter fails as well, the exception will // be just as descriptive - detected = null; - } + detected = null; + } return detected != null ? Charset.forName(detected.name()) : Charset.defaultCharset(); } @@ -1120,7 +1118,7 @@ public ISet capabilities(ISourceLocation loc) { if (watchers.hasNativeSupport(scheme)) { result.insert(vf.constructor(watchCap)); } - + return result.done(); } From a3f80138dbe9ab0e75b629c329d0362205ed1e7a Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:55:01 +0200 Subject: [PATCH 13/17] Simplify condition to decide whether or not to throw an exception when resolution fails --- .../library/lang/rascal/tests/library/IO.rsc | 12 +++++++++--- src/org/rascalmpl/uri/URIResolverRegistry.java | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc index 42007e8a351..8bc9db9b6e3 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc @@ -26,8 +26,14 @@ test bool testLogicalLocationResolution() { try { // Register authorities `foo` and `bar` - registerLocations(scheme, "foo", (|://foo/|: |file:///|)); - registerLocations(scheme, "bar", (|://bar/|: |://foo/|)); + registerLocations(scheme, "foo", ( + |://foo/|: |file:///|, + |://foo/x/y/z|: |file:///x/y/z| + )); + registerLocations(scheme, "bar", ( + |://bar/|: |://foo/|, + |://bar/x/y/z|: |://foo/x/y/z| + )); assert lastModified(|://foo/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; assert lastModified(|://bar/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; @@ -45,7 +51,7 @@ test bool testLogicalLocationResolution() { // Register default authority registerLocations(scheme, "", ( |://baz/|: |://bar/|, - |://baz/x/y/z|: |file:///x/y/z| + |://baz/x/y/z|: |://bar/x/y/z| )); assert lastModified(|://foo/|) == lastModified(|file:///|) : "Resolution unexpectedly failed"; diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index ce500fdee03..cd4a96b6073 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -400,7 +400,7 @@ private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IO } var resolved = resolveAndFixOffsets(loc, resolver); - if (resolved == null && resolver != resolversByAuth.get(auth)) { // Tried default resolver, but it failed + if (resolved == null && !auth.equals("")) { throw new UnsupportedAuthorityException(scheme, auth); } From ecbd0630ecc9790dfca6a5fbbdbcca1a482dc787 Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:55:40 +0200 Subject: [PATCH 14/17] Reorganize map operations to improve performance --- src/org/rascalmpl/uri/URIResolverRegistry.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index cd4a96b6073..3a8a44ae8c5 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -388,13 +388,16 @@ else if (loc.hasOffsetLength()) { // the logical location offsets relative to th */ private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IOException { var scheme = loc.getScheme(); + var resolversByAuth = logicalResolvers.get(scheme); // Case: `loc` is known to be logical and has a local resolver - if (logicalResolvers.containsKey(scheme)) { + if (resolversByAuth != null) { var auth = loc.getAuthority(); - var resolversByAuth = logicalResolvers.getOrDefault(scheme, Collections.emptyMap()); - var resolver = resolversByAuth.getOrDefault(auth, resolversByAuth.get("")); + var resolver = resolversByAuth.get(auth); + if (resolver == null) { + resolver = resolversByAuth.get(""); + } if (resolver == null) { throw new UnsupportedAuthorityException(scheme, auth); } From c7f176e7aa600494a8d9414c2393c8668e75a423 Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:05:18 +0200 Subject: [PATCH 15/17] Simplify condition to decide whether or not to throw an exception when resolution fails --- src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc | 4 ++-- src/org/rascalmpl/uri/URIResolverRegistry.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc index 8bc9db9b6e3..2fda6212235 100644 --- a/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc +++ b/src/org/rascalmpl/library/lang/rascal/tests/library/IO.rsc @@ -63,8 +63,8 @@ test bool testLogicalLocationResolution() { assert throwsExceptionDownstream(value() { return lastModified(|://baz/x/y/z|); }) : cause; assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/|); }) : cause; assert throwsUnsupportedAuthority(value() { return lastModified(|://qux/x/y/z|); }) : cause; - assert throwsExceptionDownstream(value() { return lastModified(|:///|); }) : cause; - assert throwsExceptionDownstream(value() { return lastModified(|:///x/y/z|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///|); }) : cause; + assert throwsUnsupportedAuthority(value() { return lastModified(|:///x/y/z|); }) : cause; return true; } diff --git a/src/org/rascalmpl/uri/URIResolverRegistry.java b/src/org/rascalmpl/uri/URIResolverRegistry.java index 3a8a44ae8c5..29cdf73cb85 100644 --- a/src/org/rascalmpl/uri/URIResolverRegistry.java +++ b/src/org/rascalmpl/uri/URIResolverRegistry.java @@ -403,7 +403,7 @@ private ISourceLocation physicalLocation(@NonNull ISourceLocation loc) throws IO } var resolved = resolveAndFixOffsets(loc, resolver); - if (resolved == null && !auth.equals("")) { + if (resolved == null) { throw new UnsupportedAuthorityException(scheme, auth); } From 43eeeca46eb8af75c0dd8baac8dba0d6fdfee8de Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:11:38 +0200 Subject: [PATCH 16/17] Update documentation --- src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java index dd807c50b32..2f6fe715f7a 100644 --- a/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java +++ b/src/org/rascalmpl/uri/ILogicalSourceLocationResolver.java @@ -18,8 +18,8 @@ public interface ILogicalSourceLocationResolver { /** - * @return A non-null location if this is an authority-specific resolver. Possibly null only if this is a default - * resolver and a default for {@code input} isn't available. + * @return {@code null} only if the authority of {@code input} is not supported/covered by this resolver. + * Non-{@code null} otherwise. */ @Nullable ISourceLocation resolve(ISourceLocation input) throws IOException; From b7a9954b661fc0ce3ce0e8bd62417511f5a708c4 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Wed, 30 Sep 2026 23:06:14 +0200 Subject: [PATCH 17/17] Fix message of `UnsupportedAuthorityException` --- src/org/rascalmpl/uri/UnsupportedAuthorityException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/rascalmpl/uri/UnsupportedAuthorityException.java b/src/org/rascalmpl/uri/UnsupportedAuthorityException.java index 1053bff1836..0d746754fc6 100644 --- a/src/org/rascalmpl/uri/UnsupportedAuthorityException.java +++ b/src/org/rascalmpl/uri/UnsupportedAuthorityException.java @@ -32,6 +32,6 @@ public class UnsupportedAuthorityException extends IOException { private static final long serialVersionUID = -3261531203667050736L; public UnsupportedAuthorityException(String scheme, String authority) { - super("Unsupported authority '" + authority + "' for scheme '\" + scheme + \"'"); + super("Unsupported authority '" + authority + "' for scheme '" + scheme + "'"); } }