Improve handling of evaling dependency notation URIs (#1595)

This commit is contained in:
Daniel Chao
2026-05-15 15:51:09 -07:00
committed by GitHub
parent 3ad1cb3645
commit a7a64acbac
17 changed files with 123 additions and 156 deletions
@@ -16,6 +16,8 @@
package org.pkl.core;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collection;
@@ -33,9 +35,11 @@ import org.msgpack.core.MessagePack;
import org.pkl.core.ast.ConstantValueNode;
import org.pkl.core.ast.internal.ToStringNodeGen;
import org.pkl.core.evaluatorSettings.TraceMode;
import org.pkl.core.externalreader.ExternalReaderProcessException;
import org.pkl.core.http.HttpClient;
import org.pkl.core.module.ModuleKeyFactory;
import org.pkl.core.module.ProjectDependenciesManager;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.packages.PackageResolver;
import org.pkl.core.project.DeclaredDependencies;
import org.pkl.core.resource.ResourceReader;
@@ -56,6 +60,7 @@ import org.pkl.core.runtime.VmUtils;
import org.pkl.core.runtime.VmValue;
import org.pkl.core.runtime.VmValueRenderer;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable;
public final class EvaluatorImpl implements Evaluator {
@@ -69,6 +74,7 @@ public final class EvaluatorImpl implements Evaluator {
private final BufferedLogger logger;
private final PackageResolver packageResolver;
private final VmValueRenderer vmValueRenderer = VmValueRenderer.singleLine(1000);
private final @Nullable URI projectFileUri;
private @Nullable MessageBufferPacker messagePacker;
public EvaluatorImpl(
@@ -94,6 +100,11 @@ public final class EvaluatorImpl implements Evaluator {
moduleResolver = new ModuleResolver(factories);
this.logger = new BufferedLogger(logger);
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
if (projectDependencies != null) {
this.projectFileUri = projectDependencies.projectFileUri();
} else {
this.projectFileUri = null;
}
polyglotContext =
VmUtils.createContext(
() -> {
@@ -424,10 +435,37 @@ public final class EvaluatorImpl implements Evaluator {
return evalResult;
}
/** Resolve dependency notation URIs (e.g. `@foo/bar.pkl`) to its resolved absolute URI. */
private ModuleSource normalizeModuleSource(ModuleSource moduleSource) {
if (moduleSource.getContents() != null
|| moduleSource.getUri().isAbsolute()
|| !moduleSource.getUri().getPath().startsWith("@")) {
return moduleSource;
}
try {
if (projectFileUri != null) {
var moduleKey = moduleResolver.resolve(projectFileUri);
var uri = IoUtils.resolve(securityManager, moduleKey, moduleSource.getUri());
return ModuleSource.uri(uri);
} else {
throw new PackageLoadError("cannotResolveDependencyNoProject");
}
} catch (URISyntaxException e) {
// impossible condition
throw PklBugException.unreachableCode();
} catch (IOException e) {
throw new VmExceptionBuilder()
.evalError("ioErrorLoadingModule", moduleSource.getUri())
.build();
} catch (ExternalReaderProcessException | SecurityManagerException | PackageLoadError e) {
throw new VmExceptionBuilder().withCause(e).build();
}
}
private <T> T doEvaluate(ModuleSource moduleSource, Function<VmTyped, T> doEvaluate) {
return doEvaluate(
() -> {
var moduleKey = moduleResolver.resolve(moduleSource);
var moduleKey = moduleResolver.resolve(normalizeModuleSource(moduleSource));
var module = VmLanguage.get(null).loadModule(moduleKey);
return doEvaluate.apply(module);
});
@@ -817,10 +817,10 @@ invalidModuleOutput=\
Expected `{0}` of module `{3}` to be of type `{1}`, but got type `{2}`.
cannotResolveDependencyWithoutHierarchicalUris=\
Cannot import dependency because project URI `{0}` does not have a hierarchical path.
Cannot resolve dependency because project URI `{0}` does not have a hierarchical path.
cannotResolveDependencyNoProject=\
Cannot import dependency because there is no project found.\n\
Cannot resolve dependency because there is no project found.\n\
\n\
If you meant to import a path that starts with `@`, prefix the path with `./` (e.g. `import "./@myPath").\n\
If you meant to import a dependency, ensure that this file is within a directory that contains a PklProject module.
@@ -1,5 +1,5 @@
–– Pkl Error ––
Cannot import dependency because there is no project found.
Cannot resolve dependency because there is no project found.
If you meant to import a path that starts with `@`, prefix the path with `./` (e.g. `import "./@myPath").
If you meant to import a dependency, ensure that this file is within a directory that contains a PklProject module.
@@ -507,7 +507,7 @@ class EvaluatorTest {
val evaluator = evaluatorBuilder.setProjectDependencies(project.dependencies).build()
assertThatCode { evaluator.use { it.evaluateOutputText(uri("foobar:baz")) } }
.hasMessageContaining(
"Cannot import dependency because project URI `foobar:foo/PklProject` does not have a hierarchical path."
"Cannot resolve dependency because project URI `foobar:foo/PklProject` does not have a hierarchical path."
)
}
@@ -722,6 +722,34 @@ class EvaluatorTest {
}
}
@Test
fun `eval dependency notation as a module source`(@TempDir tempDir: Path) {
PackageServer.populateCacheDir(tempDir)
val project = Project.load(modulePath("org/pkl/core/project/project5/PklProject"))
val evaluator =
with(EvaluatorBuilder.preconfigured()) {
moduleCacheDir = tempDir
applyFromProject(project)
build()
}
val outputText = evaluator.evaluateOutputText(uri("@fruit/catalog/apple.pkl"))
assertThat(outputText)
.isEqualTo(
"""
name = "Apple"
"""
.trimIndent()
)
}
@Test
fun `eval dependency notation -- no project configured`() {
val evaluator = Evaluator.preconfigured()
assertThatCode { evaluator.evaluateOutputText(uri("@fruit/catalog/apple.pkl")) }
.hasMessageContaining("Cannot resolve dependency because there is no project found.")
}
private fun checkModule(module: PModule) {
assertThat(module.properties.size).isEqualTo(2)
assertThat(module.getProperty("name")).isEqualTo("pigeon")