diff --git a/docs/modules/language-reference/pages/index.adoc b/docs/modules/language-reference/pages/index.adoc index fcebf7d0..bd0839ae 100644 --- a/docs/modules/language-reference/pages/index.adoc +++ b/docs/modules/language-reference/pages/index.adoc @@ -5407,7 +5407,7 @@ package { packageZipUrl = "https://example.com/\(name)/\(name)@\(version).zip" // <4> } ---- -<1> The display name of the package. For display purposes only. +<1> The display name of the package.For display purposes only. <2> The package URI, without the version part. <3> The version of the package. <4> The URL to download the package's ZIP file. @@ -5415,8 +5415,9 @@ package { The package itself is created by the command xref:pkl-cli:index.adoc#command-project-package[`pkl project package`]. This command only prepares artifacts to be published. -Once the artifacts are prepared, they are expected to be uploaded to an HTTPS server such that the ZIP asset can be downloaded at path `packageZipUrl`, and the metadata can be downloaded at `+https://+`. +Once the artifacts are prepared, they are expected to be uploaded to an HTTPS server such that the ZIP asset can be downloaded at path `packageZipUrl`, and the metadata can be downloaded at `+https://+`. +[[local-dependencies]] ==== Local dependencies A project can depend on a local project as a dependency. diff --git a/docs/modules/pkl-cli/pages/index.adoc b/docs/modules/pkl-cli/pages/index.adoc index 938c13cc..2ba3897e 100644 --- a/docs/modules/pkl-cli/pages/index.adoc +++ b/docs/modules/pkl-cli/pages/index.adoc @@ -558,14 +558,64 @@ package already exists in the cache directory, this command is a no-op. This command accepts <>. +[[command-analyze-imports]] +=== `pkl analyze imports` + +*Synopsis*: `pkl analyze imports []` + +This command builds a graph of imports declared in the provided modules. + +This is a lower level command that is meant to be useful for Pkl-related tooling. +For example, this command feeds into the xref:pkl-gradle:index.adoc[] to determine if tasks are considered up-to-date or not. + +This command produces an object with two properties, `imports` and `resolvedImports`. + +The `imports` property is a mapping of a module's absolute URI, to the set of imports declared within that module. + +The `resolvedImports` property is a mapping of a module's absolute URI (as stated in `imports`), to the resolved absolute URI that might be useful for fetching the module's contents. +For example, a xref:language-reference:index.adoc#local-dependencies[local dependency] import will have an in-language URI with scheme `projectpackage:`, and may have resolved URI with scheme `file:` (assuming that the project is file-based). + +Examples: + +[source,shell] +---- +# Analyze the imports of a single module +pkl analyze imports myModule.pkl + +# Same as the previous command, but output in JSON. +pkl analyze imports -f json myModule.pkl + +# Analyze imports of all modules declared within src/ +pkl analyze imports src/*.pkl +---- + +:: +The absolute or relative URIs of the modules to analyze. Relative URIs are resolved against the working directory. + +==== Options + +.-f, --format +[%collapsible] +==== +Same meaning as <> in <>. +==== + +.-o, --output-path +[%collapsible] +==== +Same meaning as <> in <>. +==== + +This command also takes <>. + [[common-options]] === Common options -The <>, <>, <>, <>, <>, and <> commands support the following common options: +The <>, <>, <>, <>, <>, <>, and <> commands support the following common options: include::../../pkl-cli/partials/cli-common-options.adoc[] -The <>, <>, <>, and <> commands also take the following options: +The <>, <>, <>, <>, and <> commands also take the following options: include::../../pkl-cli/partials/cli-project-options.adoc[] diff --git a/docs/modules/pkl-cli/partials/cli-common-options.adoc b/docs/modules/pkl-cli/partials/cli-common-options.adoc index 836102f8..a8509711 100644 --- a/docs/modules/pkl-cli/partials/cli-common-options.adoc +++ b/docs/modules/pkl-cli/partials/cli-common-options.adoc @@ -7,7 +7,6 @@ Comma-separated list of URI patterns that determine which modules can be loaded Patterns are matched against the beginning of module URIs. (File paths have been converted to `file:` URLs at this stage.) At least one pattern needs to match for a module to be loadable. -Both source modules and transitive modules are subject to this check. ==== [[allowed-resources]] diff --git a/docs/modules/pkl-gradle/pages/index.adoc b/docs/modules/pkl-gradle/pages/index.adoc index 5bf28703..373e5ba1 100644 --- a/docs/modules/pkl-gradle/pages/index.adoc +++ b/docs/modules/pkl-gradle/pages/index.adoc @@ -102,7 +102,6 @@ pkl { evaluators { evalPkl { sourceModules.add(file("module1.pkl")) - transitiveModules.from file("module2.pkl") outputFile = layout.buildDirectory.file("module1.yaml") outputFormat = "yaml" } @@ -118,7 +117,6 @@ pkl { evaluators { register("evalPkl") { sourceModules.add(file("module1.pkl")) - transitiveModules.from(file("module2.pkl")) outputFile.set(layout.buildDirectory.file("module1.yaml")) outputFormat.set("yaml") } @@ -127,9 +125,6 @@ pkl { ---- ==== -To guarantee correct Gradle up-to-date behavior, -`transitiveModules` needs to contain all module files transitively referenced by `sourceModules`. - For each declared evaluator, the Pkl plugin creates an equally named task. Hence the above evaluator can be run with: @@ -691,3 +686,61 @@ The project directories to create packages for. Common properties: include::../partials/gradle-common-properties.adoc[] + +[[analyze-imports]] +== Analyze Imports + +This feature is the Gradle analogy for the xref:pkl-cli:index.adoc#command-analyze-imports[analyze imports] command in the CLI. It builds a graph of imports of the provided source modules. + +=== Usage + +[tabs] +==== +build.gradle:: ++ +[source,groovy] +---- +pkl { + analyzers { + imports { + appConfig { + sourceModules.add(file("src/main/resources/appConfig.pkl")) + } + } + } +} +---- + +build.gradle.kts:: ++ +[source,kotlin] +---- +pkl { + analyzers { + imports { + register("appConfig") { + sourceModules.add(file("src/main/resources/appConfig.pkl")) + } + } + } +} +---- +==== + +=== Configuration Options + +.outputFormat: Property +[%collapsible] +==== +Same meaning as <> in <>. +==== + +.outputFile: RegularFileProperty +[%collapsible] +==== +Same meaning as <> in <>. +==== + +Common properties: + +include::../partials/gradle-modules-properties.adoc[] diff --git a/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc b/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc index eff89f9d..deaf5117 100644 --- a/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc +++ b/docs/modules/pkl-gradle/partials/gradle-common-properties.adoc @@ -7,7 +7,6 @@ URI patterns that determine which modules can be loaded and evaluated. Patterns are matched against the beginning of module URIs. (File paths have been converted to `file:` URLs at this stage.) At least one pattern needs to match for a module to be loadable. -Both source modules and transitive modules are subject to this check. ==== .allowedResources: ListProperty diff --git a/docs/modules/pkl-gradle/partials/gradle-modules-properties.adoc b/docs/modules/pkl-gradle/partials/gradle-modules-properties.adoc index 059397ee..4f145793 100644 --- a/docs/modules/pkl-gradle/partials/gradle-modules-properties.adoc +++ b/docs/modules/pkl-gradle/partials/gradle-modules-properties.adoc @@ -20,11 +20,17 @@ This property accepts the following types to represent a module: .transitiveModules: ConfigurableFileCollection [%collapsible] ==== -Default: `files()` (empty collection) + +Default: [computed by pkl-gradle] + Example 1: `transitiveModules.from files("module1.pkl", "module2.pkl")` + Example 2: `+transitiveModules.from fileTree("config").include("**/*.pkl")+` + + File paths of modules that are directly or indirectly used by source modules. -Setting this option enables correct Gradle up-to-date checks, which ensures that your Pkl tasks are executed if any of the transitive files are modified; it does not affect evaluation otherwise. + +This property, along with `sourceModules`, is the set of input files used to determine whether this task is up-to-date or not. + +By default, Pkl computes this property by analyzing the imports of the source modules. +Setting this property explicitly causes Pkl to skip the analyze imports step. + Including source modules in `transitiveModules` is permitted but not required. Relative paths are resolved against the project directory. ==== diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliImportAnalyzer.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliImportAnalyzer.kt new file mode 100644 index 00000000..5737bc1c --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliImportAnalyzer.kt @@ -0,0 +1,79 @@ +/** + * 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.cli + +import java.io.Writer +import org.pkl.commons.cli.CliCommand +import org.pkl.commons.createParentDirectories +import org.pkl.commons.writeString +import org.pkl.core.ModuleSource +import org.pkl.core.module.ModuleKeyFactories + +class CliImportAnalyzer +@JvmOverloads +constructor( + private val options: CliImportAnalyzerOptions, + private val consoleWriter: Writer = System.out.writer() +) : CliCommand(options.base) { + + override fun doRun() { + val rendered = render() + if (options.outputPath != null) { + options.outputPath.createParentDirectories() + options.outputPath.writeString(rendered) + } else { + consoleWriter.write(rendered) + consoleWriter.flush() + } + } + + // language=pkl + private val sourceModule = + ModuleSource.text( + """ + import "pkl:analyze" + + local importStrings = read*("prop:pkl.analyzeImports.**").toMap().values.toSet() + + output { + value = analyze.importGraph(importStrings) + renderer { + converters { + [Map] = (it) -> it.toMapping() + [Set] = (it) -> it.toListing() + } + } + } + """ + .trimIndent() + ) + + private fun render(): String { + val builder = evaluatorBuilder().setOutputFormat(options.outputFormat) + try { + return builder + .apply { + for ((idx, sourceModule) in options.base.normalizedSourceModules.withIndex()) { + addExternalProperty("pkl.analyzeImports.$idx", sourceModule.toString()) + } + } + .build() + .use { it.evaluateOutputText(sourceModule) } + } finally { + ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories) + } + } +} diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliImportAnalyzerOptions.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliImportAnalyzerOptions.kt new file mode 100644 index 00000000..0994333b --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliImportAnalyzerOptions.kt @@ -0,0 +1,34 @@ +/** + * 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.cli + +import java.nio.file.Path +import org.pkl.commons.cli.CliBaseOptions + +data class CliImportAnalyzerOptions( + /** Base options shared between CLI commands. */ + val base: CliBaseOptions, + + /** The file path where the output file is placed. */ + val outputPath: Path? = null, + + /** + * The output format to generate. + * + * These accept the same options as [CliEvaluatorOptions.outputFormat]. + */ + val outputFormat: String? = null, +) diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt index dd099c73..958d297b 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt @@ -34,7 +34,8 @@ internal fun main(args: Array) { ServerCommand(helpLink), TestCommand(helpLink), ProjectCommand(helpLink), - DownloadPackageCommand(helpLink) + DownloadPackageCommand(helpLink), + AnalyzeCommand(helpLink), ) .main(args) } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/AnalyzeCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/AnalyzeCommand.kt new file mode 100644 index 00000000..0b09ffa0 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/AnalyzeCommand.kt @@ -0,0 +1,66 @@ +/** + * 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.cli.commands + +import com.github.ajalt.clikt.core.NoOpCliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.path +import java.nio.file.Path +import org.pkl.cli.CliImportAnalyzer +import org.pkl.cli.CliImportAnalyzerOptions +import org.pkl.commons.cli.commands.ModulesCommand +import org.pkl.commons.cli.commands.single + +class AnalyzeCommand(helpLink: String) : + NoOpCliktCommand( + name = "analyze", + help = "Commands related to static analysis", + epilog = "For more information, visit $helpLink" + ) { + init { + subcommands(AnalyzeImportsCommand(helpLink)) + } + + companion object { + class AnalyzeImportsCommand(helpLink: String) : + ModulesCommand( + name = "imports", + helpLink = helpLink, + help = "Prints the the graph of modules imported by the input module(s)." + ) { + + private val outputPath: Path? by + option( + names = arrayOf("-o", "--output-path"), + metavar = "", + help = "File path where the output file is placed." + ) + .path() + .single() + + override fun run() { + val options = + CliImportAnalyzerOptions( + base = baseOptions.baseOptions(modules, projectOptions), + outputFormat = baseOptions.format, + outputPath = outputPath + ) + CliImportAnalyzer(options).run() + } + } + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliImportAnalyzerTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliImportAnalyzerTest.kt new file mode 100644 index 00000000..7d8d895d --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliImportAnalyzerTest.kt @@ -0,0 +1,142 @@ +/** + * 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.cli + +import java.net.URI +import java.nio.file.Path +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.pkl.commons.cli.CliBaseOptions +import org.pkl.commons.writeString +import org.pkl.core.OutputFormat +import org.pkl.core.util.StringBuilderWriter + +class CliImportAnalyzerTest { + @Test + fun `write to console writer`(@TempDir tempDir: Path) { + val file = tempDir.resolve("test.pkl").writeString("import \"bar.pkl\"") + val otherFile = tempDir.resolve("bar.pkl").writeString("") + val baseOptions = CliBaseOptions(sourceModules = listOf(file.toUri())) + val sb = StringBuilder() + val analyzer = CliImportAnalyzer(CliImportAnalyzerOptions(baseOptions), StringBuilderWriter(sb)) + analyzer.run() + assertThat(sb.toString()) + .isEqualTo( + """ + imports { + ["${otherFile.toUri()}"] {} + ["${file.toUri()}"] { + new { + uri = "${otherFile.toUri()}" + } + } + } + resolvedImports { + ["${otherFile.toUri()}"] = "${otherFile.toRealPath().toUri()}" + ["${file.toUri()}"] = "${file.toRealPath().toUri()}" + } + + """ + .trimIndent() + ) + } + + @Test + fun `different output format`(@TempDir tempDir: Path) { + val file = tempDir.resolve("test.pkl").writeString("import \"bar.pkl\"") + val otherFile = tempDir.resolve("bar.pkl").writeString("") + val baseOptions = CliBaseOptions(sourceModules = listOf(file.toUri())) + val sb = StringBuilder() + val analyzer = + CliImportAnalyzer( + CliImportAnalyzerOptions(baseOptions, outputFormat = OutputFormat.JSON.toString()), + StringBuilderWriter(sb) + ) + analyzer.run() + assertThat(sb.toString()) + .isEqualTo( + """ + { + "imports": { + "${otherFile.toUri()}": [], + "${file.toUri()}": [ + { + "uri": "${otherFile.toUri()}" + } + ] + }, + "resolvedImports": { + "${otherFile.toUri()}": "${otherFile.toRealPath().toUri()}", + "${file.toUri()}": "${file.toRealPath().toUri()}" + } + } + + """ + .trimIndent() + ) + } + + @Test + fun `write to output file`(@TempDir tempDir: Path) { + val file = tempDir.resolve("test.pkl").writeString("import \"bar.pkl\"") + val otherFile = tempDir.resolve("bar.pkl").writeString("") + val outputPath = tempDir.resolve("imports.pcf") + val baseOptions = CliBaseOptions(sourceModules = listOf(file.toUri())) + val analyzer = CliImportAnalyzer(CliImportAnalyzerOptions(baseOptions, outputPath = outputPath)) + analyzer.run() + assertThat(outputPath) + .hasContent( + """ + imports { + ["${otherFile.toUri()}"] {} + ["${file.toUri()}"] { + new { + uri = "${otherFile.toUri()}" + } + } + } + resolvedImports { + ["${otherFile.toUri()}"] = "${otherFile.toRealPath().toUri()}" + ["${file.toUri()}"] = "${file.toRealPath().toUri()}" + } + + """ + .trimIndent() + ) + } + + @Test + fun `invalid syntax in module`(@TempDir tempDir: Path) { + val file = tempDir.resolve("test.pkl").writeString("foo = bar(]") + assertThatCode { + CliImportAnalyzer( + CliImportAnalyzerOptions( + CliBaseOptions(sourceModules = listOf(file.toUri()), settings = URI("pkl:settings")) + ) + ) + .run() + } + .hasMessageContaining( + """ + –– Pkl Error –– + Found a syntax error when parsing module `${file.toUri()}`. + """ + .trimIndent() + ) + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt index 00a0f562..8ee7119a 100644 --- a/pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/CliMainTest.kt @@ -27,6 +27,7 @@ import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.condition.DisabledOnOs import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.io.TempDir +import org.pkl.cli.commands.AnalyzeCommand import org.pkl.cli.commands.EvalCommand import org.pkl.cli.commands.RootCommand import org.pkl.commons.writeString @@ -34,7 +35,8 @@ import org.pkl.commons.writeString class CliMainTest { private val evalCmd = EvalCommand("") - private val cmd = RootCommand("pkl", "pkl version 1", "").subcommands(evalCmd) + private val analyzeCommand = AnalyzeCommand("") + private val cmd = RootCommand("pkl", "pkl version 1", "").subcommands(evalCmd, analyzeCommand) @Test fun `duplicate CLI option produces meaningful error message`(@TempDir tempDir: Path) { diff --git a/pkl-core/src/main/java/org/pkl/core/Analyzer.java b/pkl-core/src/main/java/org/pkl/core/Analyzer.java new file mode 100644 index 00000000..5d5c5778 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/Analyzer.java @@ -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 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. + * + *

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))); + }); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java b/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java index af0789d3..a7622443 100644 --- a/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/EvaluatorBuilder.java @@ -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. * diff --git a/pkl-core/src/main/java/org/pkl/core/ImportGraph.java b/pkl-core/src/main/java/org/pkl/core/ImportGraph.java new file mode 100644 index 00000000..a92fb949 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ImportGraph.java @@ -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. + *

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. + *

For example, a local package dependency is represented with scheme {@code + * projectpackage:}, and (typically) resolves to a {@code file:} scheme. + */ +public record ImportGraph(Map> imports, Map resolvedImports) { + /** + * Java representation of {@code pkl.analyze#Import}. + * + * @param uri The absolute URI of the import. + */ + public record Import(URI uri) implements Comparable { + @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> parseImports(Json.JsObject jsObject) + throws JsonParseException { + var ret = new TreeMap>(); + for (var entry : jsObject.entrySet()) { + try { + var key = new URI(entry.getKey()); + var value = entry.getValue(); + var set = new TreeSet(); + 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 parseResolvedImports(Json.JsObject jsObject) + throws JsonParseException { + var ret = new TreeMap(); + 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; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/builder/ImportsAndReadsParser.java b/pkl-core/src/main/java/org/pkl/core/ast/builder/ImportsAndReadsParser.java index fd59eb1c..93e3c247 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/builder/ImportsAndReadsParser.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/builder/ImportsAndReadsParser.java @@ -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; *

  • read expressions * */ -public final class ImportsAndReadsParser - extends AbstractAstBuilder<@Nullable List>> { +public class ImportsAndReadsParser extends AbstractAstBuilder<@Nullable List> { + + 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> parse( + public static @Nullable List 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> visitModuleExtendsOrAmendsClause( + public @Nullable List 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> visitImportClause(ImportClauseContext ctx) { + public List 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> visitImportExpr(ImportExprContext ctx) { + public List 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> visitReadExpr(ReadExprContext ctx) { + public List 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> aggregateResult( - @Nullable List> aggregate, - @Nullable List> nextResult) { + protected @Nullable List aggregateResult( + @Nullable List aggregate, @Nullable List nextResult) { if (aggregate == null || aggregate.isEmpty()) { return nextResult; } if (nextResult == null || nextResult.isEmpty()) { return aggregate; } - var ret = new ArrayList>(aggregate.size() + nextResult.size()); + var ret = new ArrayList(aggregate.size() + nextResult.size()); ret.addAll(aggregate); ret.addAll(nextResult); return ret; diff --git a/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java b/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java index 4a44230c..5ed77497 100644 --- a/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java +++ b/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java @@ -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) { diff --git a/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java index 0e7c21a7..a0611322 100644 --- a/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java +++ b/pkl-core/src/main/java/org/pkl/core/project/ProjectPackager.java @@ -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> getImportsAndReads(Path pklModulePath) { + private @Nullable List getImportsAndReads(Path pklModulePath) { try { var moduleKey = ModuleKeys.file(pklModulePath.toUri()); var resolvedModuleKey = ResolvedModuleKeys.file(moduleKey, moduleKey.getUri(), pklModulePath); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/AnalyzeModule.java b/pkl-core/src/main/java/org/pkl/core/runtime/AnalyzeModule.java new file mode 100644 index 00000000..d702925f --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/AnalyzeModule.java @@ -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)); + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java index 93083779..1cdbe4b0 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ModuleCache.java @@ -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(); diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java index d0ad42b2..f6ed7619 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmException.java @@ -94,6 +94,7 @@ public abstract class VmException extends AbstractTruffleException { public enum Kind { EVAL_ERROR, UNDEFINED_VALUE, + WRAPPED, BUG } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java index aa9e7378..bb69ccaf 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionBuilder.java @@ -53,6 +53,7 @@ public final class VmExceptionBuilder { private @Nullable Object receiver; private @Nullable Map 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 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); }; } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java index ee90a9b0..095d5b09 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmExceptionRenderer.java @@ -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) { diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmImportAnalyzer.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmImportAnalyzer.java new file mode 100644 index 00000000..7afefbb0 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmImportAnalyzer.java @@ -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>(); + var resolvedImports = new TreeMap(); + for (var moduleUri : moduleUris) { + analyzeSingle(moduleUri, context, imports, resolvedImports); + } + return new ImportGraph(imports, resolvedImports); + } + + @TruffleBoundary + private static void analyzeSingle( + URI moduleUri, + VmContext context, + Map> imports, + Map 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 collectImports( + URI moduleUri, ModuleResolver moduleResolver, SecurityManager securityManager) + throws IOException, URISyntaxException, SecurityManagerException { + var moduleKey = moduleResolver.resolve(moduleUri); + var resolvedModuleKey = moduleKey.resolve(securityManager); + List 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(); + 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; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmWrappedEvalException.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmWrappedEvalException.java new file mode 100644 index 00000000..b9a40460 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmWrappedEvalException.java @@ -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 programValues, + @Nullable Node location, + @Nullable SourceSection sourceSection, + @Nullable String memberName, + @Nullable String hint, + Map insertedStackFrames, + VmException wrappedException) { + super( + message, + cause, + isExternalMessage, + messageArguments, + programValues, + location, + sourceSection, + memberName, + hint, + insertedStackFrames); + this.wrappedException = wrappedException; + } + + public VmException getWrappedException() { + return wrappedException; + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/analyze/AnalyzeNodes.java b/pkl-core/src/main/java/org/pkl/core/stdlib/analyze/AnalyzeNodes.java new file mode 100644 index 00000000..67195723 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/analyze/AnalyzeNodes.java @@ -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 importFactory = + new VmObjectFactory(AnalyzeModule::getImportClass) + .addStringProperty("uri", (it) -> it.uri().toString()); + + private static VmObjectFactory importGraphFactory = + new VmObjectFactory(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(); + } + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/stdlib/analyze/package-info.java b/pkl-core/src/main/java/org/pkl/core/stdlib/analyze/package-info.java new file mode 100644 index 00000000..b27066f8 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/stdlib/analyze/package-info.java @@ -0,0 +1,4 @@ +@NonnullByDefault +package org.pkl.core.stdlib.analyze; + +import org.pkl.core.util.NonnullByDefault; diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index debe015b..e46c4d11 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -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://[:]`): `{0}`. + +cannotAnalyzeBecauseSyntaxError=\ +Found a syntax error when parsing module `{0}`. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/a.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/a.pkl new file mode 100644 index 00000000..53c98632 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/a.pkl @@ -0,0 +1 @@ +import "b.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/b.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/b.pkl new file mode 100644 index 00000000..e69de29b diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/cyclicalA.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/cyclicalA.pkl new file mode 100644 index 00000000..8e226150 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/cyclicalA.pkl @@ -0,0 +1 @@ +import "cyclicalB.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/cyclicalB.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/cyclicalB.pkl new file mode 100644 index 00000000..b1c3317c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/cyclicalB.pkl @@ -0,0 +1 @@ +import "cyclicalA.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/globImport.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/globImport.pkl new file mode 100644 index 00000000..502f0cef --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input-helper/analyze/globImport.pkl @@ -0,0 +1 @@ +import* "[ab].pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/api/analyze1.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/api/analyze1.pkl new file mode 100644 index 00000000..d94c04e0 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/api/analyze1.pkl @@ -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() + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeInvalidHttpModule.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeInvalidHttpModule.pkl new file mode 100644 index 00000000..1b0181b8 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeInvalidHttpModule.pkl @@ -0,0 +1,3 @@ +import "pkl:analyze" + +result = analyze.importGraph(Set("http://localhost:0/foo.pkl")) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeInvalidModuleUri.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeInvalidModuleUri.pkl new file mode 100644 index 00000000..081315fe --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeInvalidModuleUri.pkl @@ -0,0 +1,3 @@ +import "pkl:analyze" + +result = analyze.importGraph(Set("foo <>")) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeRelativeModuleUri.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeRelativeModuleUri.pkl new file mode 100644 index 00000000..cc1e1a0e --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/analyzeRelativeModuleUri.pkl @@ -0,0 +1,3 @@ +import "pkl:analyze" + +result = analyze.importGraph(Set("foo.pkl")) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/api/analyze1.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/api/analyze1.pcf new file mode 100644 index 00000000..f7af40de --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/api/analyze1.pcf @@ -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" + } + } + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeInvalidHttpModule.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeInvalidHttpModule.err new file mode 100644 index 00000000..45d1460c --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeInvalidHttpModule.err @@ -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) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeInvalidModuleUri.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeInvalidModuleUri.err new file mode 100644 index 00000000..d3fe651a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeInvalidModuleUri.err @@ -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) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeRelativeModuleUri.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeRelativeModuleUri.err new file mode 100644 index 00000000..74e490d1 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/analyzeRelativeModuleUri.err @@ -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) diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err index 19a3834d..7026b690 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/cannotFindStdLibModule.err @@ -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 diff --git a/pkl-core/src/test/kotlin/org/pkl/core/AnalyzerTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/AnalyzerTest.kt new file mode 100644 index 00000000..1feea622 --- /dev/null +++ b/pkl-core/src/test/kotlin/org/pkl/core/AnalyzerTest.kt @@ -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() +} diff --git a/pkl-core/src/test/kotlin/org/pkl/core/ast/builder/ImportsAndReadsParserTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/ast/builder/ImportsAndReadsParserTest.kt index ef2b9737..040f6f14 100644 --- a/pkl-core/src/test/kotlin/org/pkl/core/ast/builder/ImportsAndReadsParserTest.kt +++ b/pkl-core/src/test/kotlin/org/pkl/core/ast/builder/ImportsAndReadsParserTest.kt @@ -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 { + ImportsAndReadsParser.parse(moduleKey, moduleKey.resolve(SecurityManagers.defaultManager)) + } + assertThat(err.toPklException(StackFrameTransformers.defaultTransformer)) + .hasMessage( + """ + –– Pkl Error –– + Mismatched input: ``. Expected one of: `{`, `=`, `:` + + 1 | not valid Pkl syntax + ^ + at text (repl:text) + + """ + .trimIndent() + ) + } } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/PklAnalyzerCommands.java b/pkl-gradle/src/main/java/org/pkl/gradle/PklAnalyzerCommands.java new file mode 100644 index 00000000..77b1a87d --- /dev/null +++ b/pkl-gradle/src/main/java/org/pkl/gradle/PklAnalyzerCommands.java @@ -0,0 +1,28 @@ +/** + * 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.gradle; + +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.pkl.gradle.spec.AnalyzeImportsSpec; + +public interface PklAnalyzerCommands { + NamedDomainObjectContainer getImports(); + + default void imports(Action> action) { + action.execute(getImports()); + } +} diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/PklExtension.java b/pkl-gradle/src/main/java/org/pkl/gradle/PklExtension.java index 3a3eef34..f61cc268 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/PklExtension.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/PklExtension.java @@ -39,6 +39,9 @@ public interface PklExtension { @Nested PklProjectCommands getProject(); + @Nested + PklAnalyzerCommands getAnalyzers(); + default void evaluators(Action> action) { action.execute(getEvaluators()); } @@ -64,4 +67,8 @@ public interface PklExtension { default void project(Action action) { action.execute(getProject()); } + + default void analyzers(Action action) { + action.execute(getAnalyzers()); + } } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java index 663d38b8..f0208778 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java @@ -17,6 +17,7 @@ package org.pkl.gradle; import java.io.File; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; @@ -36,8 +37,12 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin; import org.gradle.plugins.ide.idea.model.IdeaModel; import org.gradle.util.GradleVersion; import org.pkl.cli.CliEvaluatorOptions; +import org.pkl.core.ImportGraph; +import org.pkl.core.OutputFormat; import org.pkl.core.util.IoUtils; import org.pkl.core.util.LateInit; +import org.pkl.core.util.Nullable; +import org.pkl.gradle.spec.AnalyzeImportsSpec; import org.pkl.gradle.spec.BasePklSpec; import org.pkl.gradle.spec.CodeGenSpec; import org.pkl.gradle.spec.EvalSpec; @@ -48,6 +53,7 @@ import org.pkl.gradle.spec.PkldocSpec; import org.pkl.gradle.spec.ProjectPackageSpec; import org.pkl.gradle.spec.ProjectResolveSpec; import org.pkl.gradle.spec.TestSpec; +import org.pkl.gradle.task.AnalyzeImportsTask; import org.pkl.gradle.task.BasePklTask; import org.pkl.gradle.task.CodeGenTask; import org.pkl.gradle.task.EvalTask; @@ -87,6 +93,7 @@ public class PklPlugin implements Plugin { configureTestTasks(extension.getTests()); configureProjectPackageTasks(extension.getProject().getPackagers()); configureProjectResolveTasks(extension.getProject().getResolvers()); + configureAnalyzeImportsTasks(extension.getAnalyzers().getImports()); } private void configureProjectPackageTasks(NamedDomainObjectContainer specs) { @@ -128,6 +135,21 @@ public class PklPlugin implements Plugin { }); } + private void configureAnalyzeImportsTasks(NamedDomainObjectContainer specs) { + specs.all( + spec -> { + configureBaseSpec(spec); + spec.getOutputFormat().convention(OutputFormat.PCF.toString()); + var analyzeImportsTask = createTask(AnalyzeImportsTask.class, spec); + analyzeImportsTask.configure( + task -> { + task.getOutputFormat().set(spec.getOutputFormat()); + task.getOutputFile().set(spec.getOutputFile()); + configureModulesTask(task, spec, null); + }); + }); + } + private void configureEvalTasks(NamedDomainObjectContainer specs) { specs.all( spec -> { @@ -141,7 +163,7 @@ public class PklPlugin implements Plugin { // and the working directory is set to the project directory, // so this path works correctly. .file("%{moduleDir}/%{moduleName}.%{outputFormat}")); - spec.getOutputFormat().convention("pcf"); + spec.getOutputFormat().convention(OutputFormat.PCF.toString()); spec.getModuleOutputSeparator() .convention(CliEvaluatorOptions.Companion.getDefaults().getModuleOutputSeparator()); spec.getExpression() @@ -431,20 +453,75 @@ public class PklPlugin implements Plugin { task.getHttpNoProxy().set(spec.getHttpNoProxy()); } - private void configureModulesTask(T task, S spec) { + private List getTransitiveModules(AnalyzeImportsTask analyzeTask) { + var outputFile = analyzeTask.getOutputFile().get().getAsFile().toPath(); + try { + var contents = Files.readString(outputFile); + ImportGraph importGraph = ImportGraph.parseFromJson(contents); + var imports = importGraph.resolvedImports().values(); + return imports.stream() + .filter((it) -> it.getScheme().equalsIgnoreCase("file")) + .map(File::new) + .toList(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void configureModulesTask( + T task, S spec, @Nullable TaskProvider analyzeImportsTask) { configureBaseTask(task, spec); task.getSourceModules().set(spec.getSourceModules()); - task.getTransitiveModules().from(spec.getTransitiveModules()); task.getNoProject().set(spec.getNoProject()); task.getProjectDir().set(spec.getProjectDir()); task.getOmitProjectSettings().set(spec.getOmitProjectSettings()); + if (!spec.getTransitiveModules().isEmpty()) { + task.getTransitiveModules().set(spec.getTransitiveModules()); + } else if (analyzeImportsTask != null) { + task.dependsOn(analyzeImportsTask); + task.getTransitiveModules().set(analyzeImportsTask.map(this::getTransitiveModules)); + } } - private TaskProvider createModulesTask( - Class taskClass, ModulesSpec spec) { + private TaskProvider createAnalyzeImportsTask(ModulesSpec spec) { + var outputFile = + project + .getLayout() + .getBuildDirectory() + .file("pkl-gradle/imports/" + spec.getName() + ".json"); return project .getTasks() - .register(spec.getName(), taskClass, task -> configureModulesTask(task, spec)); + .register( + spec.getName() + "GatherImports", + AnalyzeImportsTask.class, + task -> { + configureModulesTask(task, spec, null); + task.setDescription("Compute the set of imports declared by input modules"); + task.setGroup("build"); + task.getOutputFormat().set(OutputFormat.JSON.toString()); + task.getOutputFile().set(outputFile); + }); + } + + /** + * Implicitly also create a task of type {@link AnalyzeImportsTask}, postfixing the spec name with + * {@code "GatherImports"}. + * + *

    The resulting task depends on the analyze task, and configures its own input files based on + * the result of analysis. + * + *

    The end result is that the task automatically has correct up-to-date checks without users + * needing to manually provide transitive modules. + */ + private TaskProvider createModulesTask( + Class taskClass, ModulesSpec spec) { + var analyzeImportsTask = createAnalyzeImportsTask(spec); + return project + .getTasks() + .register( + spec.getName(), + taskClass, + task -> configureModulesTask(task, spec, analyzeImportsTask)); } private TaskProvider createTask(Class taskClass, BasePklSpec spec) { diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/spec/AnalyzeImportsSpec.java b/pkl-gradle/src/main/java/org/pkl/gradle/spec/AnalyzeImportsSpec.java new file mode 100644 index 00000000..8a27030a --- /dev/null +++ b/pkl-gradle/src/main/java/org/pkl/gradle/spec/AnalyzeImportsSpec.java @@ -0,0 +1,26 @@ +/** + * 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.gradle.spec; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; + +/** Configuration options for import analyzers. Documented in user manual. */ +public interface AnalyzeImportsSpec extends ModulesSpec { + RegularFileProperty getOutputFile(); + + Property getOutputFormat(); +} diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java new file mode 100644 index 00000000..04b98964 --- /dev/null +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/AnalyzeImportsTask.java @@ -0,0 +1,52 @@ +/** + * 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.gradle.task; + +import java.io.File; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.pkl.cli.CliImportAnalyzer; +import org.pkl.cli.CliImportAnalyzerOptions; + +public abstract class AnalyzeImportsTask extends ModulesTask { + @OutputFile + @Optional + public abstract RegularFileProperty getOutputFile(); + + @Input + public abstract Property getOutputFormat(); + + private final Provider cliImportAnalyzerProvider = + getProviders() + .provider( + () -> + new CliImportAnalyzer( + new CliImportAnalyzerOptions( + getCliBaseOptions(), + mapAndGetOrNull(getOutputFile(), it -> it.getAsFile().toPath()), + mapAndGetOrNull(getOutputFormat(), it -> it)))); + + @Override + protected void doRunTask() { + //noinspection ResultOfMethodCallIgnored + getOutputs().getPreviousOutputFiles().forEach(File::delete); + cliImportAnalyzerProvider.get().run(); + } +} diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java index 254b44fb..f7c2b905 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.gradle.api.InvalidUserDataException; -import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; import org.gradle.api.provider.ListProperty; @@ -49,7 +48,7 @@ public abstract class ModulesTask extends BasePklTask { public abstract ListProperty getSourceModules(); @InputFiles - public abstract ConfigurableFileCollection getTransitiveModules(); + public abstract ListProperty getTransitiveModules(); private final Map, Pair, List>> parsedSourceModulesCache = new HashMap<>(); diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/AnalyzeImportsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/AnalyzeImportsTest.kt new file mode 100644 index 00000000..34c6ce13 --- /dev/null +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/AnalyzeImportsTest.kt @@ -0,0 +1,108 @@ +/** + * 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.gradle + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class AnalyzeImportsTest : AbstractTest() { + @Test + fun `write to console`() { + writeFile("input.pkl", "") + writeFile( + "build.gradle", + """ + plugins { + id "org.pkl-lang" + } + + pkl { + analyzers { + imports { + analyzeMyImports { + sourceModules = ["input.pkl"] + } + } + } + } + """ + .trimIndent() + ) + val result = runTask("analyzeMyImports") + assertThat(result.output).contains("imports {") + } + + @Test + fun `output file`() { + writeFile("input.pkl", "") + writeFile( + "build.gradle", + """ + plugins { + id "org.pkl-lang" + } + + pkl { + analyzers { + imports { + analyzeMyImports { + sourceModules = ["input.pkl"] + outputFile = file("myFile.pcf") + } + } + } + } + """ + .trimIndent() + ) + runTask("analyzeMyImports") + assertThat(testProjectDir.resolve("myFile.pcf")).exists() + } + + @Test + fun `output format`() { + writeFile("input.pkl", "") + writeFile( + "build.gradle", + """ + plugins { + id "org.pkl-lang" + } + + pkl { + analyzers { + imports { + analyzeMyImports { + sourceModules = ["input.pkl"] + outputFormat = "json" + } + } + } + } + """ + .trimIndent() + ) + val result = runTask("analyzeMyImports") + assertThat(result.output) + .contains( + """ + { + "imports": { + """ + .trimIndent() + ) + } +} diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt index c07aa663..76ef174f 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt @@ -839,6 +839,62 @@ class EvaluatorsTest : AbstractTest() { ) } + @Test + fun `implicit dependency tracking for declared imports`() { + writePklFile("import \"shared.pkl\"") + writeFile("shared.pkl", "foo = 1") + writeBuildFile("json") + val result1 = runTask("evalTest") + assertThat(result1.task(":evalTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + + // evalTest should be up-to-date now + val result2 = runTask("evalTest") + assertThat(result2.task(":evalTest")!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + + // update transitive module with new contents + writeFile("shared.pkl", "foo = 2") + + // evalTest should be out-of-date and need to run again + val result3 = runTask("evalTest") + assertThat(result3.task(":evalTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + + // running again should be up-to-date again + val result4 = runTask("evalTest") + assertThat(result4.task(":evalTest")!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + } + + @Test + fun `explicit dependency tracking using transitive modules`() { + writePklFile("import \"shared.pkl\"") + writeFile("shared.pkl", "foo = 1") + writeFile("shared2.pkl", "foo = 1") + // intentionally use wrong transitive module + writeBuildFile( + "json", + additionalContents = + """ + transitiveModules.from(files("shared2.pkl")) + """ + .trimIndent() + ) + val result1 = runTask("evalTest") + assertThat(result1.task(":evalTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + + // evalTest should be up-to-date now + val result2 = runTask("evalTest") + assertThat(result2.task(":evalTest")!!.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + + // update transitive module with new contents + writeFile("shared2.pkl", "foo = 2") + + // evalTest should be out-of-date and need to run again + val result5 = runTask("evalTest") + assertThat(result5.task(":evalTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + + // the "GatherImports" task did not run + assertThat(result5.task(":evalTestGatherImports")).isNull() + } + private fun writeBuildFile( // don't use `org.pkl.core.OutputFormat` // because test compile class path doesn't contain pkl-core diff --git a/stdlib/analyze.pkl b/stdlib/analyze.pkl new file mode 100644 index 00000000..c74d0424 --- /dev/null +++ b/stdlib/analyze.pkl @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// 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. +//===----------------------------------------------------------------------===// + +/// A library for statically analyzing Pkl modules. +/// +/// These tools differentiate from [pkl:reflect][reflect] in that they parse Pkl modules, but do not +/// execute any code within these modules. +@Since { version = "0.27.0" } +@ModuleInfo { minPklVersion = "0.27.0" } +module pkl.analyze + +// used by doc comments +import "pkl:reflect" + +/// Given a set of Pkl module URIs, returns a graph of imports declared by these modules. +/// +/// The resulting graph includes transitive imports. +external function importGraph(moduleUris: Set): ImportGraph + +/// The graph of imports declared (directly and transitively) by the modules passed to +/// [importGraph()]. +class ImportGraph { + /// The imports declared within a Pkl program. + /// + /// Each entry maps a module URI to the set of imports declared in that module. + /// + /// The set of all modules in the graph can be obtained via its [keys][Map.keys]. + imports: Map> + + /// Mappings of modules from their in-language URI, to their resolved URI. + /// + /// A module's in-language URI is the form used within Pkl source code. + /// For example, modulepath-based modules have form `modulepath:/path/to/my/module.pkl`. + /// + /// A module's resolved URI is the form used to load the module's contents. + /// The same modulepath module might have form + /// `jar:file:///path/to/file.zip!/path/to/my/module.pkl` if Pkl run with + /// `--module-path /path/to/file.zip`. + /// + /// Dependency-notation imports, such as `"@myPackage/myModule.pkl"`, are represented as + /// in-language URIs with scheme `projectpackage:`. + /// In the case of local project dependenecies, they will be local URIs resolved from the project + /// file URI (in normal cases, `file:` URIs). + resolvedImports: Map(keys == imports.keys) +} + +/// An import as declared inside a module. +class Import { + /// The absolute (in-language) URI of the import. + /// + /// Dependency notation URIs (such as `import "@foo/bar"`) are resolved to package URIs with + /// scheme `projectpackage:`. + uri: Uri +}