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

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

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

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

View File

@@ -0,0 +1,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()
)
}
}

View File

@@ -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) {