Add analyze imports libs (SPICE-0001) (#695)

This adds a new feature to build a dependency graph of Pkl programs, following the SPICE outlined in https://github.com/apple/pkl-evolution/pull/2.

It adds:
* CLI command `pkl analyze imports`
* Java API `org.pkl.core.Analyzer`
* Pkl stdlib module `pkl:analyze`
* pkl-gradle extension `analyze`

In addition, it also changes the Gradle plugin such that `transitiveModules` is by default computed from the import graph.
This commit is contained in:
Daniel Chao
2024-10-23 14:36:57 -07:00
committed by GitHub
parent eb3891b21f
commit ce25cb8ef0
53 changed files with 2054 additions and 53 deletions

View File

@@ -0,0 +1,120 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.graalvm.polyglot.Context;
import org.pkl.core.http.HttpClient;
import org.pkl.core.http.HttpClientInitException;
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.runtime.ModuleResolver;
import org.pkl.core.runtime.ResourceManager;
import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmException;
import org.pkl.core.runtime.VmImportAnalyzer;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.util.Nullable;
/** Utility library for static analysis of Pkl programs. */
public class Analyzer {
private final StackFrameTransformer transformer;
private final SecurityManager securityManager;
private final @Nullable Path moduleCacheDir;
private final @Nullable DeclaredDependencies projectDependencies;
private final ModuleResolver moduleResolver;
private final HttpClient httpClient;
public Analyzer(
StackFrameTransformer transformer,
SecurityManager securityManager,
Collection<ModuleKeyFactory> moduleKeyFactories,
@Nullable Path moduleCacheDir,
@Nullable DeclaredDependencies projectDependencies,
HttpClient httpClient) {
this.transformer = transformer;
this.securityManager = securityManager;
this.moduleCacheDir = moduleCacheDir;
this.projectDependencies = projectDependencies;
this.moduleResolver = new ModuleResolver(moduleKeyFactories);
this.httpClient = httpClient;
}
/**
* Builds a graph of imports from the provided source modules.
*
* <p>For details, see {@link ImportGraph}.
*/
public ImportGraph importGraph(URI... sources) {
var context = createContext();
try {
context.enter();
var vmContext = VmContext.get(null);
return VmImportAnalyzer.analyze(sources, vmContext);
} catch (SecurityManagerException
| IOException
| URISyntaxException
| PackageLoadError
| HttpClientInitException e) {
throw new PklException(e.getMessage(), e);
} catch (PklException err) {
throw err;
} catch (VmException err) {
throw err.toPklException(transformer);
} catch (Exception e) {
throw new PklBugException(e);
} finally {
context.leave();
context.close();
}
}
private Context createContext() {
var packageResolver =
PackageResolver.getInstance(
securityManager, HttpClient.builder().buildLazily(), moduleCacheDir);
return VmUtils.createContext(
() -> {
VmContext vmContext = VmContext.get(null);
vmContext.initialize(
new VmContext.Holder(
transformer,
securityManager,
httpClient,
moduleResolver,
new ResourceManager(securityManager, List.of()),
Loggers.stdErr(),
Map.of(),
Map.of(),
moduleCacheDir,
null,
packageResolver,
projectDependencies == null
? null
: new ProjectDependenciesManager(
projectDependencies, moduleResolver, securityManager)));
});
}
}

View File

@@ -433,6 +433,10 @@ public final class EvaluatorBuilder {
return this;
}
public @Nullable DeclaredDependencies getProjectDependencies() {
return this.dependencies;
}
/**
* Given a project, sets its dependencies, and also applies any evaluator settings if set.
*

View File

@@ -0,0 +1,111 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.pkl.core.util.json.Json;
import org.pkl.core.util.json.Json.FormatException;
import org.pkl.core.util.json.Json.JsArray;
import org.pkl.core.util.json.Json.JsObject;
import org.pkl.core.util.json.Json.JsonParseException;
import org.pkl.core.util.json.Json.MappingException;
/**
* Java representation of {@code pkl.analyze#ImportGraph}.
*
* @param imports The graph of imports declared within the program.
* <p>Each key is a module inside the program, and each value is the module URIs declared as
* imports inside that module. The set of all dependent modules within a program is the set of
* keys in this map.
* @param resolvedImports A mapping of a module's in-language URI, and the URI that it resolves to.
* <p>For example, a local package dependency is represented with scheme {@code
* projectpackage:}, and (typically) resolves to a {@code file:} scheme.
*/
public record ImportGraph(Map<URI, Set<Import>> imports, Map<URI, URI> resolvedImports) {
/**
* Java representation of {@code pkl.analyze#Import}.
*
* @param uri The absolute URI of the import.
*/
public record Import(URI uri) implements Comparable<Import> {
@Override
public int compareTo(Import o) {
return uri.compareTo(o.uri());
}
}
/** Parses the provided JSON into an import graph. */
public static ImportGraph parseFromJson(String input) throws JsonParseException {
var parsed = Json.parseObject(input);
var imports = parseImports(parsed.getObject("imports"));
var resolvedImports = parseResolvedImports(parsed.getObject("resolvedImports"));
return new ImportGraph(imports, resolvedImports);
}
private static Map<URI, Set<Import>> parseImports(Json.JsObject jsObject)
throws JsonParseException {
var ret = new TreeMap<URI, Set<Import>>();
for (var entry : jsObject.entrySet()) {
try {
var key = new URI(entry.getKey());
var value = entry.getValue();
var set = new TreeSet<Import>();
if (!(value instanceof JsArray array)) {
throw new FormatException("array", value.getClass());
}
for (var elem : array) {
if (!(elem instanceof JsObject importObj)) {
throw new FormatException("object", elem.getClass());
}
set.add(parseImport(importObj));
}
ret.put(key, set);
} catch (URISyntaxException e) {
throw new MappingException(entry.getKey(), e);
}
}
return ret;
}
private static ImportGraph.Import parseImport(Json.JsObject jsObject) throws JsonParseException {
var uri = jsObject.getURI("uri");
return new Import(uri);
}
private static Map<URI, URI> parseResolvedImports(Json.JsObject jsObject)
throws JsonParseException {
var ret = new TreeMap<URI, URI>();
for (var entry : jsObject.entrySet()) {
try {
var key = new URI(entry.getKey());
var value = entry.getValue();
if (!(value instanceof String str)) {
throw new FormatException("string", value.getClass());
}
var valueUri = new URI(str);
ret.put(key, valueUri);
} catch (URISyntaxException e) {
throw new MappingException(entry.getKey(), e);
}
}
return ret;
}
}

View File

@@ -21,9 +21,12 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.pkl.core.ast.builder.ImportsAndReadsParser.Entry;
import org.pkl.core.module.ModuleKey;
import org.pkl.core.module.ResolvedModuleKey;
import org.pkl.core.parser.LexParseException;
import org.pkl.core.parser.Parser;
import org.pkl.core.parser.antlr.PklLexer;
import org.pkl.core.parser.antlr.PklParser.ImportClauseContext;
import org.pkl.core.parser.antlr.PklParser.ImportExprContext;
import org.pkl.core.parser.antlr.PklParser.ModuleExtendsOrAmendsClauseContext;
@@ -31,8 +34,8 @@ import org.pkl.core.parser.antlr.PklParser.ReadExprContext;
import org.pkl.core.parser.antlr.PklParser.SingleLineStringLiteralContext;
import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable;
import org.pkl.core.util.Pair;
/**
* Collects module uris and resource uris imported within a module.
@@ -46,17 +49,29 @@ import org.pkl.core.util.Pair;
* <li>read expressions
* </ul>
*/
public final class ImportsAndReadsParser
extends AbstractAstBuilder<@Nullable List<Pair<String, SourceSection>>> {
public class ImportsAndReadsParser extends AbstractAstBuilder<@Nullable List<Entry>> {
public record Entry(
boolean isModule,
boolean isGlob,
boolean isExtends,
boolean isAmends,
String stringValue,
SourceSection sourceSection) {}
/** Parses a module, and collects all imports and reads. */
public static @Nullable List<Pair<String, SourceSection>> parse(
public static @Nullable List<Entry> parse(
ModuleKey moduleKey, ResolvedModuleKey resolvedModuleKey) throws IOException {
var parser = new Parser();
var text = resolvedModuleKey.loadSource();
var source = VmUtils.createSource(moduleKey, text);
var importListParser = new ImportsAndReadsParser(source);
return parser.parseModule(text).accept(importListParser);
try {
return parser.parseModule(text).accept(importListParser);
} catch (LexParseException e) {
var moduleName = IoUtils.inferModuleName(moduleKey);
throw VmUtils.toVmException(e, source, moduleName);
}
}
public ImportsAndReadsParser(Source source) {
@@ -69,29 +84,35 @@ public final class ImportsAndReadsParser
}
@Override
public List<Pair<String, SourceSection>> visitModuleExtendsOrAmendsClause(
public @Nullable List<Entry> visitModuleExtendsOrAmendsClause(
ModuleExtendsOrAmendsClauseContext ctx) {
var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts);
var sourceSection = createSourceSection(ctx.stringConstant());
return Collections.singletonList(Pair.of(importStr, sourceSection));
return Collections.singletonList(
new Entry(
true, false, ctx.EXTENDS() != null, ctx.AMENDS() != null, importStr, sourceSection));
}
@Override
public List<Pair<String, SourceSection>> visitImportClause(ImportClauseContext ctx) {
public List<Entry> visitImportClause(ImportClauseContext ctx) {
var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts);
var sourceSection = createSourceSection(ctx.stringConstant());
return Collections.singletonList(Pair.of(importStr, sourceSection));
return Collections.singletonList(
new Entry(
true, ctx.t.getType() == PklLexer.IMPORT_GLOB, false, false, importStr, sourceSection));
}
@Override
public List<Pair<String, SourceSection>> visitImportExpr(ImportExprContext ctx) {
public List<Entry> visitImportExpr(ImportExprContext ctx) {
var importStr = doVisitSingleLineConstantStringPart(ctx.stringConstant().ts);
var sourceSection = createSourceSection(ctx.stringConstant());
return Collections.singletonList(Pair.of(importStr, sourceSection));
return Collections.singletonList(
new Entry(
true, ctx.t.getType() == PklLexer.IMPORT_GLOB, false, false, importStr, sourceSection));
}
@Override
public List<Pair<String, SourceSection>> visitReadExpr(ReadExprContext ctx) {
public List<Entry> visitReadExpr(ReadExprContext ctx) {
var expr = ctx.expr();
if (!(expr instanceof SingleLineStringLiteralContext slCtx)) {
return Collections.emptyList();
@@ -111,20 +132,26 @@ public final class ImportsAndReadsParser
} else {
return Collections.emptyList();
}
return Collections.singletonList(Pair.of(importString, createSourceSection(slCtx)));
return Collections.singletonList(
new Entry(
false,
ctx.t.getType() == PklLexer.READ_GLOB,
false,
false,
importString,
createSourceSection(slCtx)));
}
@Override
protected @Nullable List<Pair<String, SourceSection>> aggregateResult(
@Nullable List<Pair<String, SourceSection>> aggregate,
@Nullable List<Pair<String, SourceSection>> nextResult) {
protected @Nullable List<Entry> aggregateResult(
@Nullable List<Entry> aggregate, @Nullable List<Entry> nextResult) {
if (aggregate == null || aggregate.isEmpty()) {
return nextResult;
}
if (nextResult == null || nextResult.isEmpty()) {
return aggregate;
}
var ret = new ArrayList<Pair<String, SourceSection>>(aggregate.size() + nextResult.size());
var ret = new ArrayList<Entry>(aggregate.size() + nextResult.size());
ret.addAll(aggregate);
ret.addAll(nextResult);
return ret;

View File

@@ -199,6 +199,10 @@ public final class ProjectDependenciesManager {
}
}
public DeclaredDependencies getDeclaredDependencies() {
return declaredDependencies;
}
public Dependency getResolvedDependency(PackageUri packageUri) {
var dep = getProjectDeps().get(CanonicalPackageUri.fromPackageUri(packageUri));
if (dep == null) {

View File

@@ -15,7 +15,6 @@
*/
package org.pkl.core.project;
import com.oracle.truffle.api.source.SourceSection;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
@@ -64,7 +63,6 @@ import org.pkl.core.util.GlobResolver;
import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable;
import org.pkl.core.util.Pair;
/**
* Given a list of project directories, prepares artifacts to be published as a package.
@@ -397,8 +395,8 @@ public final class ProjectPackager {
return;
}
for (var importContext : imports) {
var importStr = importContext.first;
var sourceSection = importContext.second;
var importStr = importContext.stringValue();
var sourceSection = importContext.sourceSection();
if (isAbsoluteImport(importStr)) {
continue;
}
@@ -440,7 +438,7 @@ public final class ProjectPackager {
}
}
private @Nullable List<Pair<String, SourceSection>> getImportsAndReads(Path pklModulePath) {
private @Nullable List<ImportsAndReadsParser.Entry> getImportsAndReads(Path pklModulePath) {
try {
var moduleKey = ModuleKeys.file(pklModulePath.toUri());
var resolvedModuleKey = ResolvedModuleKeys.file(moduleKey, moduleKey.getUri(), pklModulePath);

View File

@@ -0,0 +1,53 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.net.URI;
public class AnalyzeModule extends StdLibModule {
private static final VmTyped instance = VmUtils.createEmptyModule();
static {
loadModule(URI.create("pkl:analyze"), instance);
}
public static VmTyped getModule() {
return instance;
}
public static VmClass getImportGraphClass() {
return AnalyzeModule.ImportGraphClass.instance;
}
public static VmClass getImportClass() {
return AnalyzeModule.ImportClass.instance;
}
private static final class ImportGraphClass {
static final VmClass instance = loadClass("ImportGraph");
}
private static final class ImportClass {
static final VmClass instance = loadClass("Import");
}
@TruffleBoundary
private static VmClass loadClass(String className) {
var theModule = getModule();
return (VmClass) VmUtils.readMember(theModule, Identifier.get(className));
}
}

View File

@@ -85,6 +85,8 @@ public final class ModuleCache {
// some standard library modules are cached as static singletons
// and hence aren't parsed/initialized anew for every evaluator
switch (moduleName) {
case "analyze":
return AnalyzeModule.getModule();
case "base":
// always needed
return BaseModule.getModule();

View File

@@ -94,6 +94,7 @@ public abstract class VmException extends AbstractTruffleException {
public enum Kind {
EVAL_ERROR,
UNDEFINED_VALUE,
WRAPPED,
BUG
}

View File

@@ -53,6 +53,7 @@ public final class VmExceptionBuilder {
private @Nullable Object receiver;
private @Nullable Map<CallTarget, StackFrame> insertedStackFrames;
private VmException wrappedException;
public static class MultilineValue {
private final Iterable<?> lines;
@@ -332,6 +333,12 @@ public final class VmExceptionBuilder {
return this;
}
public VmExceptionBuilder wrapping(VmException nestedException) {
this.wrappedException = nestedException;
this.kind = VmException.Kind.WRAPPED;
return this;
}
public VmExceptionBuilder withInsertedStackFrames(
Map<CallTarget, StackFrame> insertedStackFrames) {
this.insertedStackFrames = insertedStackFrames;
@@ -383,6 +390,19 @@ public final class VmExceptionBuilder {
memberName,
hint,
effectiveInsertedStackFrames);
case WRAPPED ->
new VmWrappedEvalException(
message,
cause,
isExternalMessage,
messageArguments,
programValues,
location,
sourceSection,
memberName,
hint,
effectiveInsertedStackFrames,
wrappedException);
};
}

View File

@@ -19,6 +19,7 @@ import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import org.pkl.core.Release;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable;
@@ -46,7 +47,7 @@ public final class VmExceptionRenderer {
if (exception instanceof VmBugException bugException) {
renderBugException(bugException, builder);
} else {
renderException(exception, builder);
renderException(exception, builder, true);
}
}
@@ -66,13 +67,13 @@ public final class VmExceptionRenderer {
.replaceAll("\\+", "%20"));
builder.append("\n\n");
renderException(exception, builder);
renderException(exception, builder, true);
builder.append('\n').append(Release.current().versionInfo()).append("\n\n");
exceptionToReport.printStackTrace(new PrintWriter(new StringBuilderWriter(builder)));
}
private void renderException(VmException exception, StringBuilder builder) {
private void renderException(VmException exception, StringBuilder builder, boolean withHeader) {
var header = " Pkl Error ";
String message;
@@ -94,7 +95,16 @@ public final class VmExceptionRenderer {
message = exception.getMessage();
}
builder.append(header).append('\n').append(message).append('\n');
if (withHeader) {
builder.append(header).append('\n');
}
builder.append(message).append('\n');
if (exception instanceof VmWrappedEvalException vmWrappedEvalException) {
var sb = new StringBuilder();
renderException(vmWrappedEvalException.getWrappedException(), sb, false);
hint = sb.toString().lines().map((it) -> ">\t" + it).collect(Collectors.joining("\n"));
}
// include cause's message unless it's the same as this exception's message
if (exception.getCause() != null) {

View File

@@ -0,0 +1,125 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.pkl.core.ImportGraph;
import org.pkl.core.ImportGraph.Import;
import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.ast.builder.ImportsAndReadsParser;
import org.pkl.core.ast.builder.ImportsAndReadsParser.Entry;
import org.pkl.core.util.GlobResolver;
import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
import org.pkl.core.util.GlobResolver.ResolvedGlobElement;
import org.pkl.core.util.IoUtils;
public class VmImportAnalyzer {
@TruffleBoundary
public static ImportGraph analyze(URI[] moduleUris, VmContext context)
throws IOException, URISyntaxException, SecurityManagerException {
var imports = new TreeMap<URI, Set<ImportGraph.Import>>();
var resolvedImports = new TreeMap<URI, URI>();
for (var moduleUri : moduleUris) {
analyzeSingle(moduleUri, context, imports, resolvedImports);
}
return new ImportGraph(imports, resolvedImports);
}
@TruffleBoundary
private static void analyzeSingle(
URI moduleUri,
VmContext context,
Map<URI, Set<ImportGraph.Import>> imports,
Map<URI, URI> resolvedImports)
throws IOException, URISyntaxException, SecurityManagerException {
var moduleResolver = context.getModuleResolver();
var securityManager = context.getSecurityManager();
var importsInModule = collectImports(moduleUri, moduleResolver, securityManager);
imports.put(moduleUri, importsInModule);
resolvedImports.put(
moduleUri, moduleResolver.resolve(moduleUri).resolve(securityManager).getUri());
for (var imprt : importsInModule) {
if (imports.containsKey(imprt.uri())) {
continue;
}
analyzeSingle(imprt.uri(), context, imports, resolvedImports);
}
}
private static Set<ImportGraph.Import> collectImports(
URI moduleUri, ModuleResolver moduleResolver, SecurityManager securityManager)
throws IOException, URISyntaxException, SecurityManagerException {
var moduleKey = moduleResolver.resolve(moduleUri);
var resolvedModuleKey = moduleKey.resolve(securityManager);
List<Entry> importsAndReads;
try {
importsAndReads = ImportsAndReadsParser.parse(moduleKey, resolvedModuleKey);
} catch (VmException err) {
throw new VmExceptionBuilder()
.evalError("cannotAnalyzeBecauseSyntaxError", moduleKey.getUri())
.wrapping(err)
.build();
}
if (importsAndReads == null) {
return Set.of();
}
var result = new TreeSet<ImportGraph.Import>();
for (var entry : importsAndReads) {
if (!entry.isModule()) {
continue;
}
if (entry.isGlob()) {
var theModuleKey =
moduleResolver.resolve(moduleKey.resolveUri(IoUtils.toUri(entry.stringValue())));
try {
var elements =
GlobResolver.resolveGlob(
securityManager,
theModuleKey,
moduleKey,
moduleKey.getUri(),
entry.stringValue());
var globImports =
elements.values().stream()
.map(ResolvedGlobElement::getUri)
.map(ImportGraph.Import::new)
.toList();
result.addAll(globImports);
} catch (InvalidGlobPatternException e) {
throw new VmExceptionBuilder()
.evalError("invalidGlobPattern", entry.stringValue())
.withSourceSection(entry.sourceSection())
.build();
}
} else {
var resolvedUri =
IoUtils.resolve(securityManager, moduleKey, IoUtils.toUri(entry.stringValue()));
result.add(new Import(resolvedUri));
}
}
return result;
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
import java.util.List;
import java.util.Map;
import org.pkl.core.StackFrame;
import org.pkl.core.util.Nullable;
public class VmWrappedEvalException extends VmEvalException {
private final VmException wrappedException;
public VmWrappedEvalException(
String message,
@Nullable Throwable cause,
boolean isExternalMessage,
Object[] messageArguments,
List<ProgramValue> programValues,
@Nullable Node location,
@Nullable SourceSection sourceSection,
@Nullable String memberName,
@Nullable String hint,
Map<CallTarget, StackFrame> insertedStackFrames,
VmException wrappedException) {
super(
message,
cause,
isExternalMessage,
messageArguments,
programValues,
location,
sourceSection,
memberName,
hint,
insertedStackFrames);
this.wrappedException = wrappedException;
}
public VmException getWrappedException() {
return wrappedException;
}
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.stdlib.analyze;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.pkl.core.ImportGraph;
import org.pkl.core.ImportGraph.Import;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.AnalyzeModule;
import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmImportAnalyzer;
import org.pkl.core.runtime.VmMap;
import org.pkl.core.runtime.VmSet;
import org.pkl.core.runtime.VmTyped;
import org.pkl.core.stdlib.ExternalMethod1Node;
import org.pkl.core.stdlib.VmObjectFactory;
public final class AnalyzeNodes {
private AnalyzeNodes() {}
private static VmObjectFactory<Import> importFactory =
new VmObjectFactory<Import>(AnalyzeModule::getImportClass)
.addStringProperty("uri", (it) -> it.uri().toString());
private static VmObjectFactory<ImportGraph> importGraphFactory =
new VmObjectFactory<ImportGraph>(AnalyzeModule::getImportGraphClass)
.addMapProperty(
"imports",
graph -> {
var builder = VmMap.builder();
for (var entry : graph.imports().entrySet()) {
var vmSetBuilder = VmSet.EMPTY.builder();
for (var imprt : entry.getValue()) {
vmSetBuilder.add(importFactory.create(imprt));
}
builder.add(entry.getKey().toString(), vmSetBuilder.build());
}
return builder.build();
})
.addMapProperty(
"resolvedImports",
graph -> {
var builder = VmMap.builder();
for (var entry : graph.resolvedImports().entrySet()) {
builder.add(entry.getKey().toString(), entry.getValue().toString());
}
return builder.build();
});
public abstract static class importGraph extends ExternalMethod1Node {
@Specialization
@TruffleBoundary
protected Object eval(@SuppressWarnings("unused") VmTyped self, VmSet moduleUris) {
var uris = new URI[moduleUris.getLength()];
var idx = 0;
for (var moduleUri : moduleUris) {
URI uri;
try {
uri = new URI((String) moduleUri);
} catch (URISyntaxException e) {
throw exceptionBuilder()
.evalError("invalidModuleUri", moduleUri)
.withHint(e.getMessage())
.build();
}
if (!uri.isAbsolute()) {
throw exceptionBuilder().evalError("cannotAnalyzeRelativeModuleUri", moduleUri).build();
}
uris[idx] = uri;
idx++;
}
var context = VmContext.get(this);
try {
var results = VmImportAnalyzer.analyze(uris, context);
return importGraphFactory.create(results);
} catch (IOException | URISyntaxException | SecurityManagerException | PackageLoadError e) {
throw exceptionBuilder().withCause(e).build();
}
}
}
}

View File

@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.core.stdlib.analyze;
import org.pkl.core.util.NonnullByDefault;

View File

@@ -635,6 +635,9 @@ Expected an exception, but none was thrown.
cannotEvaluateRelativeModuleUri=\
Cannot evaluate relative module URI `{0}`.
cannotAnalyzeRelativeModuleUri=\
Cannot analyze relative module URI `{0}`.
invalidModuleUri=\
Module URI `{0}` has invalid syntax.
@@ -1060,3 +1063,6 @@ To fix this problem, add dependendy `org.pkl:pkl-certs`.
# suppress inspection "HttpUrlsUsage"
malformedProxyAddress=\
Malformed proxy URI (expecting `http://<host>[:<port>]`): `{0}`.
cannotAnalyzeBecauseSyntaxError=\
Found a syntax error when parsing module `{0}`.

View File

@@ -0,0 +1 @@
import "b.pkl"

View File

@@ -0,0 +1 @@
import "cyclicalB.pkl"

View File

@@ -0,0 +1 @@
import "cyclicalA.pkl"

View File

@@ -0,0 +1 @@
import* "[ab].pkl"

View File

@@ -0,0 +1,33 @@
amends "../snippetTest.pkl"
import "pkl:analyze"
import "pkl:reflect"
import ".../input-helper/analyze/a.pkl"
import ".../input-helper/analyze/cyclicalA.pkl"
import ".../input-helper/analyze/globImport.pkl"
examples {
["basic"] {
analyze.importGraph(Set(reflect.Module(a).uri))
}
["cycles"] {
analyze.importGraph(Set(reflect.Module(cyclicalA).uri))
}
["globs"] {
analyze.importGraph(Set(reflect.Module(globImport).uri))
}
["packages"] {
analyze.importGraph(Set("package://localhost:0/birds@0.5.0#/Bird.pkl"))
}
}
output {
renderer {
// mimick result of `pkl analyze imports` CLI command
converters {
[Map] = (it) -> it.toMapping()
[Set] = (it) -> it.toListing()
}
}
}

View File

@@ -0,0 +1,3 @@
import "pkl:analyze"
result = analyze.importGraph(Set("http://localhost:0/foo.pkl"))

View File

@@ -0,0 +1,3 @@
import "pkl:analyze"
result = analyze.importGraph(Set("foo <>"))

View File

@@ -0,0 +1,3 @@
import "pkl:analyze"
result = analyze.importGraph(Set("foo.pkl"))

View File

@@ -0,0 +1,79 @@
examples {
["basic"] {
new {
imports {
["file:///$snippetsDir/input-helper/analyze/a.pkl"] {
new {
uri = "file:///$snippetsDir/input-helper/analyze/b.pkl"
}
}
["file:///$snippetsDir/input-helper/analyze/b.pkl"] {}
}
resolvedImports {
["file:///$snippetsDir/input-helper/analyze/a.pkl"] = "file:///$snippetsDir/input-helper/analyze/a.pkl"
["file:///$snippetsDir/input-helper/analyze/b.pkl"] = "file:///$snippetsDir/input-helper/analyze/b.pkl"
}
}
}
["cycles"] {
new {
imports {
["file:///$snippetsDir/input-helper/analyze/cyclicalA.pkl"] {
new {
uri = "file:///$snippetsDir/input-helper/analyze/cyclicalB.pkl"
}
}
["file:///$snippetsDir/input-helper/analyze/cyclicalB.pkl"] {
new {
uri = "file:///$snippetsDir/input-helper/analyze/cyclicalA.pkl"
}
}
}
resolvedImports {
["file:///$snippetsDir/input-helper/analyze/cyclicalA.pkl"] = "file:///$snippetsDir/input-helper/analyze/cyclicalA.pkl"
["file:///$snippetsDir/input-helper/analyze/cyclicalB.pkl"] = "file:///$snippetsDir/input-helper/analyze/cyclicalB.pkl"
}
}
}
["globs"] {
new {
imports {
["file:///$snippetsDir/input-helper/analyze/a.pkl"] {
new {
uri = "file:///$snippetsDir/input-helper/analyze/b.pkl"
}
}
["file:///$snippetsDir/input-helper/analyze/b.pkl"] {}
["file:///$snippetsDir/input-helper/analyze/globImport.pkl"] {
new {
uri = "file:///$snippetsDir/input-helper/analyze/a.pkl"
}
new {
uri = "file:///$snippetsDir/input-helper/analyze/b.pkl"
}
}
}
resolvedImports {
["file:///$snippetsDir/input-helper/analyze/a.pkl"] = "file:///$snippetsDir/input-helper/analyze/a.pkl"
["file:///$snippetsDir/input-helper/analyze/b.pkl"] = "file:///$snippetsDir/input-helper/analyze/b.pkl"
["file:///$snippetsDir/input-helper/analyze/globImport.pkl"] = "file:///$snippetsDir/input-helper/analyze/globImport.pkl"
}
}
}
["packages"] {
new {
imports {
["package://localhost:0/birds@0.5.0#/Bird.pkl"] {
new {
uri = "package://localhost:0/fruit@1.0.5#/Fruit.pkl"
}
}
["package://localhost:0/fruit@1.0.5#/Fruit.pkl"] {}
}
resolvedImports {
["package://localhost:0/birds@0.5.0#/Bird.pkl"] = "package://localhost:0/birds@0.5.0#/Bird.pkl"
["package://localhost:0/fruit@1.0.5#/Fruit.pkl"] = "package://localhost:0/fruit@1.0.5#/Fruit.pkl"
}
}
}
}

View File

@@ -0,0 +1,10 @@
Pkl Error
HTTP/1.1 header parser received no bytes
x | result = analyze.importGraph(Set("http://localhost:0/foo.pkl"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at analyzeInvalidHttpModule#result (file:///$snippetsDir/input/errors/analyzeInvalidHttpModule.pkl)
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)

View File

@@ -0,0 +1,12 @@
Pkl Error
Module URI `foo <>` has invalid syntax.
x | result = analyze.importGraph(Set("foo <>"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at analyzeInvalidModuleUri#result (file:///$snippetsDir/input/errors/analyzeInvalidModuleUri.pkl)
Illegal character in path at index 3: foo <>
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)

View File

@@ -0,0 +1,10 @@
Pkl Error
Cannot analyze relative module URI `foo.pkl`.
x | result = analyze.importGraph(Set("foo.pkl"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at analyzeRelativeModuleUri#result (file:///$snippetsDir/input/errors/analyzeRelativeModuleUri.pkl)
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)

View File

@@ -6,6 +6,7 @@ x | import "pkl:nonExisting"
at cannotFindStdLibModule#nonExisting (file:///$snippetsDir/input/errors/cannotFindStdLibModule.pkl)
Available standard library modules:
pkl:analyze
pkl:base
pkl:Benchmark
pkl:DocPackageInfo

View File

@@ -0,0 +1,318 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core
import java.net.URI
import java.nio.file.Path
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.createParentDirectories
import org.pkl.commons.test.PackageServer
import org.pkl.commons.writeString
import org.pkl.core.http.HttpClient
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.project.Project
class AnalyzerTest {
private val simpleAnalyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
SecurityManagers.defaultManager,
listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg),
null,
null,
HttpClient.dummyClient()
)
@Test
fun `simple case`(@TempDir tempDir: Path) {
val file =
tempDir
.resolve("test.pkl")
.writeString(
"""
amends "pkl:base"
import "pkl:json"
myProp = import("pkl:xml")
"""
.trimIndent()
)
.toUri()
val result = simpleAnalyzer.importGraph(file)
assertThat(result.imports)
.containsEntry(
file,
setOf(
ImportGraph.Import(URI("pkl:base")),
ImportGraph.Import(URI("pkl:json")),
ImportGraph.Import(URI("pkl:xml"))
)
)
}
@Test
fun `glob imports`(@TempDir tempDir: Path) {
val file1 =
tempDir
.resolve("file1.pkl")
.writeString(
"""
import* "*.pkl"
"""
.trimIndent()
)
.toUri()
val file2 = tempDir.resolve("file2.pkl").writeString("foo = 1").toUri()
val file3 = tempDir.resolve("file3.pkl").writeString("bar = 1").toUri()
val result = simpleAnalyzer.importGraph(file1)
assertThat(result.imports)
.isEqualTo(
mapOf(
file1 to
setOf(ImportGraph.Import(file1), ImportGraph.Import(file2), ImportGraph.Import(file3)),
file2 to emptySet(),
file3 to emptySet()
),
)
}
@Test
fun `cyclical imports`(@TempDir tempDir: Path) {
val file1 = tempDir.resolve("file1.pkl").writeString("import \"file2.pkl\"").toUri()
val file2 = tempDir.resolve("file2.pkl").writeString("import \"file1.pkl\"").toUri()
val result = simpleAnalyzer.importGraph(file1)
assertThat(result.imports)
.isEqualTo(
mapOf(file1 to setOf(ImportGraph.Import(file2)), file2 to setOf(ImportGraph.Import(file1)))
)
}
@Test
fun `package imports`(@TempDir tempDir: Path) {
val analyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
SecurityManagers.defaultManager,
listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg),
tempDir.resolve("packages"),
null,
HttpClient.dummyClient(),
)
PackageServer.populateCacheDir(tempDir.resolve("packages"))
val file1 =
tempDir
.resolve("file1.pkl")
.writeString("import \"package://localhost:0/birds@0.5.0#/Bird.pkl\"")
.toUri()
val result = analyzer.importGraph(file1)
assertThat(result.imports)
.isEqualTo(
mapOf(
file1 to setOf(ImportGraph.Import(URI("package://localhost:0/birds@0.5.0#/Bird.pkl"))),
URI("package://localhost:0/birds@0.5.0#/Bird.pkl") to
setOf(ImportGraph.Import(URI("package://localhost:0/fruit@1.0.5#/Fruit.pkl"))),
URI("package://localhost:0/fruit@1.0.5#/Fruit.pkl") to emptySet()
)
)
}
@Test
fun `project dependency imports`(@TempDir tempDir: Path) {
tempDir
.resolve("PklProject")
.writeString(
"""
amends "pkl:Project"
dependencies {
["birds"] { uri = "package://localhost:0/birds@0.5.0" }
}
"""
.trimIndent()
)
tempDir
.resolve("PklProject.deps.json")
.writeString(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:0/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:0/birds@0.5.0",
"checksums": {
"sha256": "${'$'}skipChecksumVerification"
}
},
"package://localhost:0/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:0/fruit@1.0.5",
"checksums": {
"sha256": "${'$'}skipChecksumVerification"
}
}
}
}
"""
.trimIndent()
)
val project = Project.loadFromPath(tempDir.resolve("PklProject"))
PackageServer.populateCacheDir(tempDir.resolve("packages"))
val analyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
SecurityManagers.defaultManager,
listOf(
ModuleKeyFactories.file,
ModuleKeyFactories.standardLibrary,
ModuleKeyFactories.pkg,
ModuleKeyFactories.projectpackage
),
tempDir.resolve("packages"),
project.dependencies,
HttpClient.dummyClient()
)
val file1 =
tempDir
.resolve("file1.pkl")
.writeString(
"""
import "@birds/Bird.pkl"
"""
.trimIndent()
)
.toUri()
val result = analyzer.importGraph(file1)
assertThat(result.imports)
.isEqualTo(
mapOf(
file1 to
setOf(ImportGraph.Import(URI("projectpackage://localhost:0/birds@0.5.0#/Bird.pkl"))),
URI("projectpackage://localhost:0/birds@0.5.0#/Bird.pkl") to
setOf(ImportGraph.Import(URI("projectpackage://localhost:0/fruit@1.0.5#/Fruit.pkl"))),
URI("projectpackage://localhost:0/fruit@1.0.5#/Fruit.pkl") to emptySet()
)
)
assertThat(result.resolvedImports)
.isEqualTo(
mapOf(
file1 to file1.realPath(),
URI("projectpackage://localhost:0/birds@0.5.0#/Bird.pkl") to
URI("projectpackage://localhost:0/birds@0.5.0#/Bird.pkl"),
URI("projectpackage://localhost:0/fruit@1.0.5#/Fruit.pkl") to
URI("projectpackage://localhost:0/fruit@1.0.5#/Fruit.pkl")
)
)
}
@Test
fun `local project dependency import`(@TempDir tempDir: Path) {
val pklProject =
tempDir
.resolve("project1/PklProject")
.createParentDirectories()
.writeString(
"""
amends "pkl:Project"
dependencies {
["birds"] = import("../birds/PklProject")
}
"""
.trimIndent()
)
tempDir
.resolve("birds/PklProject")
.createParentDirectories()
.writeString(
"""
amends "pkl:Project"
package {
name = "birds"
version = "1.0.0"
packageZipUrl = "https://localhost:0/foo.zip"
baseUri = "package://localhost:0/birds"
}
"""
.trimIndent()
)
val birdModule = tempDir.resolve("birds/bird.pkl").writeString("name = \"Warbler\"")
pklProject.parent
.resolve("PklProject.deps.json")
.writeString(
"""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:0/birds@1": {
"type": "local",
"uri": "projectpackage://localhost:0/birds@1.0.0",
"path": "../birds"
}
}
}
"""
.trimIndent()
)
val mainPkl =
pklProject.parent
.resolve("main.pkl")
.writeString(
"""
import "@birds/bird.pkl"
"""
.trimIndent()
)
val project = Project.loadFromPath(pklProject)
val analyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
SecurityManagers.defaultManager,
listOf(
ModuleKeyFactories.file,
ModuleKeyFactories.standardLibrary,
ModuleKeyFactories.pkg,
ModuleKeyFactories.projectpackage
),
tempDir.resolve("packages"),
project.dependencies,
HttpClient.dummyClient()
)
val result = analyzer.importGraph(mainPkl.toUri())
val birdUri = URI("projectpackage://localhost:0/birds@1.0.0#/bird.pkl")
assertThat(result.imports)
.isEqualTo(
mapOf(mainPkl.toUri() to setOf(ImportGraph.Import(birdUri)), birdUri to emptySet()),
)
assertThat(result.resolvedImports)
.isEqualTo(
mapOf(
mainPkl.toUri() to mainPkl.toRealPath().toUri(),
birdUri to birdModule.toRealPath().toUri()
)
)
}
private fun URI.realPath() = Path.of(this).toRealPath().toUri()
}

View File

@@ -18,8 +18,11 @@ package org.pkl.core.ast.builder
import java.net.URI
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.pkl.core.SecurityManagers
import org.pkl.core.StackFrameTransformers
import org.pkl.core.module.ModuleKeys
import org.pkl.core.runtime.VmException
class ImportsAndReadsParserTest {
@Test
@@ -27,13 +30,13 @@ class ImportsAndReadsParserTest {
val moduleText =
"""
amends "foo.pkl"
import "bar.pkl"
import "bazzy/buz.pkl"
res1 = import("qux.pkl")
res2 = import*("qux/*.pkl")
class MyClass {
res3 {
res4 {
@@ -48,7 +51,7 @@ class ImportsAndReadsParserTest {
val moduleKey = ModuleKeys.synthetic(URI("repl:text"), moduleText)
val imports =
ImportsAndReadsParser.parse(moduleKey, moduleKey.resolve(SecurityManagers.defaultManager))
assertThat(imports?.map { it.first })
assertThat(imports?.map { it.stringValue })
.hasSameElementsAs(
listOf(
"foo.pkl",
@@ -62,4 +65,31 @@ class ImportsAndReadsParserTest {
)
)
}
@Test
fun `invalid syntax`() {
val moduleText =
"""
not valid Pkl syntax
"""
.trimIndent()
val moduleKey = ModuleKeys.synthetic(URI("repl:text"), moduleText)
val err =
assertThrows<VmException> {
ImportsAndReadsParser.parse(moduleKey, moduleKey.resolve(SecurityManagers.defaultManager))
}
assertThat(err.toPklException(StackFrameTransformers.defaultTransformer))
.hasMessage(
"""
Pkl Error
Mismatched input: `<EOF>`. Expected one of: `{`, `=`, `:`
1 | not valid Pkl syntax
^
at text (repl:text)
"""
.trimIndent()
)
}
}