Implement canonical formatter (#1107)

CLI commands also added: `pkl format check` and `pkl format apply`.
This commit is contained in:
Islon Scherer
2025-09-17 11:12:04 +02:00
committed by GitHub
parent 6a06ab7caa
commit fdc501a35c
78 changed files with 5491 additions and 26 deletions

View File

@@ -733,6 +733,33 @@ pkl shell-completion bash
pkl shell-completion zsh pkl shell-completion zsh
---- ----
[[command-format-check]]
=== `pkl format check`
*Synopsis*: `pkl format check <file-or-dir-path>`
This command checks for format violations on the given file or directory and print their names to stdout. +
It returns a non-zero status code in case violations are found.
If the path is a directory, recursively looks for files with a `.pkl` extension, or files named `PklProject`.
[[command-format-apply]]
=== `pkl format apply`
*Synopsis*: `pkl format apply [<options>] <file-or-dir-path>`
This command formats the given files overwriting them.
If the path is a directory, recursively looks for files with a `.pkl` extension, or files named `PklProject`.
==== Options
.-s, --silent
[%collapsible]
====
Do not write the name of wrongly formatted files to stdout.
====
[[common-options]] [[common-options]]
=== Common options === Common options

View File

@@ -61,6 +61,7 @@ dependencies {
implementation(libs.jlineTerminal) implementation(libs.jlineTerminal)
implementation(libs.jlineTerminalJansi) implementation(libs.jlineTerminalJansi)
implementation(projects.pklServer) implementation(projects.pklServer)
implementation(projects.pklFormatter)
implementation(libs.clikt) implementation(libs.clikt)
testImplementation(projects.pklCommonsTest) testImplementation(projects.pklCommonsTest)

View File

@@ -0,0 +1,52 @@
/*
* Copyright © 2025 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.IOException
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.writeText
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
class CliFormatterApply(cliBaseOptions: CliBaseOptions, path: Path, private val silent: Boolean) :
CliFormatterCommand(cliBaseOptions, path) {
override fun doRun() {
var status = 0
for (path in paths()) {
val contents = Files.readString(path)
val (formatted, stat) = format(path, contents)
status = if (status == 0) stat else status
if (stat != 0) continue
if (!silent && contents != formatted) {
consoleWriter.write(path.toAbsolutePath().toString())
consoleWriter.flush()
}
try {
path.writeText(formatted, Charsets.UTF_8)
} catch (e: IOException) {
consoleWriter.write("Could not overwrite `$path`: ${e.message}")
consoleWriter.flush()
status = 1
}
}
if (status != 0) {
throw CliException("Formatting violations found.", status)
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright © 2025 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.Files
import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
class CliFormatterCheck(cliBaseOptions: CliBaseOptions, path: Path) :
CliFormatterCommand(cliBaseOptions, path) {
override fun doRun() {
var status = 0
for (path in paths()) {
val contents = Files.readString(path)
val (formatted, stat) = format(path, contents)
status = if (status == 0) stat else status
if (contents != formatted) {
consoleWriter.write(path.toAbsolutePath().toString())
consoleWriter.flush()
status = 1
}
}
if (status != 0) {
throw CliException("Formatting violations found.", status)
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright © 2025 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 java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.extension
import kotlin.io.path.isDirectory
import kotlin.io.path.name
import kotlin.io.path.walk
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliCommand
import org.pkl.formatter.Formatter
import org.pkl.parser.GenericParserError
abstract class CliFormatterCommand
@JvmOverloads
constructor(
options: CliBaseOptions,
protected val path: Path,
protected val consoleWriter: Writer = System.out.writer(),
) : CliCommand(options) {
protected fun format(file: Path, contents: String): Pair<String, Int> {
try {
return Formatter().format(contents) to 0
} catch (pe: GenericParserError) {
consoleWriter.write("Could not format `$file`: $pe")
consoleWriter.flush()
return "" to 1
}
}
@OptIn(ExperimentalPathApi::class)
protected fun paths(): Sequence<Path> {
return if (path.isDirectory()) {
path.walk().filter { it.extension == "pkl" || it.name == "PklProject" }
} else sequenceOf(path)
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright © 2025 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.Context
import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.flag
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.CliFormatterApply
import org.pkl.cli.CliFormatterCheck
import org.pkl.commons.cli.commands.BaseCommand
class FormatterCommand : NoOpCliktCommand(name = "format") {
override fun help(context: Context) = "Run commands related to formatting"
override fun helpEpilog(context: Context) = "For more information, visit $helpLink"
init {
subcommands(FormatterCheckCommand(), FormatterApplyCommand())
}
}
class FormatterCheckCommand : BaseCommand(name = "check", helpLink = helpLink) {
override val helpString: String =
"Check if the given files are properly formatted, printing the file name to stdout in case they are not. Returns non-zero in case of failure."
val path: Path by
argument(name = "path", help = "File or directory to check.")
.path(mustExist = true, canBeDir = true)
override fun run() {
CliFormatterCheck(baseOptions.baseOptions(emptyList()), path).run()
}
}
class FormatterApplyCommand : BaseCommand(name = "apply", helpLink = helpLink) {
override val helpString: String =
"Overwrite all the files in place with the formatted version. Returns non-zero in case of failure."
val path: Path by
argument(name = "path", help = "File or directory to format.")
.path(mustExist = true, canBeDir = true)
val silent: Boolean by
option(
names = arrayOf("-s", "--silent"),
help = "Do not write the name of the files that failed formatting to stdout.",
)
.flag()
override fun run() {
CliFormatterApply(baseOptions.baseOptions(emptyList()), path, silent)
}
}

View File

@@ -49,6 +49,7 @@ class RootCommand : NoOpCliktCommand(name = "pkl") {
ProjectCommand(), ProjectCommand(),
DownloadPackageCommand(), DownloadPackageCommand(),
AnalyzeCommand(), AnalyzeCommand(),
FormatterCommand(),
CompletionCommand( CompletionCommand(
name = "shell-completion", name = "shell-completion",
help = "Generate a completion script for the given shell", help = "Generate a completion script for the given shell",

View File

@@ -0,0 +1,36 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
net.bytebuddy:byte-buddy:1.15.11=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata
org.assertj:assertj-core:3.27.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-build-common:2.0.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.0.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.0.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.0.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.0.21=kotlinBuildToolsApiClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:2.0.21=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-native-prebuilt:2.0.21=kotlinNativeBundleConfiguration
org.jetbrains.kotlin:kotlin-reflect:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-scripting-common:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:2.0.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.0.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains:annotations:13.0=compileClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.13.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.13.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:5.13.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.13.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.13.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:1.13.0=testRuntimeClasspath
org.junit:junit-bom:5.13.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
empty=annotationProcessor,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDefExtensions

View File

@@ -0,0 +1,48 @@
/*
* Copyright © 2025 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.
*/
plugins {
pklAllProjects
pklKotlinLibrary
pklPublishLibrary
}
dependencies {
api(projects.pklParser)
testImplementation(projects.pklCommonsTest)
}
tasks.test {
inputs
.dir("src/test/files/FormatterSnippetTests/input")
.withPropertyName("formatterSnippetTestsInput")
.withPathSensitivity(PathSensitivity.RELATIVE)
inputs
.dir("src/test/files/FormatterSnippetTests/output")
.withPropertyName("formatterSnippetTestsOutput")
.withPathSensitivity(PathSensitivity.RELATIVE)
}
publishing {
publications {
named<MavenPublication>("library") {
pom {
url.set("https://github.com/apple/pkl/tree/main/pkl-formatter")
description.set("Formatter for Pkl")
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
/*
* Copyright © 2025 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.formatter
import java.nio.file.Files
import java.nio.file.Path
import org.pkl.formatter.ast.ForceLine
import org.pkl.formatter.ast.Nodes
import org.pkl.parser.GenericParser
/** A formatter for Pkl files that applies canonical formatting rules. */
class Formatter {
/**
* Formats a Pkl file from the given file path.
*
* @param path the path to the Pkl file to format
* @return the formatted Pkl source code as a string
* @throws java.io.IOException if the file cannot be read
*/
fun format(path: Path): String {
return format(Files.readString(path))
}
/**
* Formats the given Pkl source code text.
*
* @param text the Pkl source code to format
* @return the formatted Pkl source code as a string
*/
fun format(text: String): String {
val parser = GenericParser()
val builder = Builder(text)
val gen = Generator()
val ast = parser.parseModule(text)
val formatAst = builder.format(ast)
// force a line at the end of the file
gen.generate(Nodes(listOf(formatAst, ForceLine)))
return gen.toString()
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright © 2025 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.formatter
import org.pkl.formatter.ast.Empty
import org.pkl.formatter.ast.ForceLine
import org.pkl.formatter.ast.ForceWrap
import org.pkl.formatter.ast.FormatNode
import org.pkl.formatter.ast.Group
import org.pkl.formatter.ast.IfWrap
import org.pkl.formatter.ast.Indent
import org.pkl.formatter.ast.Line
import org.pkl.formatter.ast.MultilineStringGroup
import org.pkl.formatter.ast.Nodes
import org.pkl.formatter.ast.Space
import org.pkl.formatter.ast.SpaceOrLine
import org.pkl.formatter.ast.Text
import org.pkl.formatter.ast.Wrap
internal class Generator {
private val buf: StringBuilder = StringBuilder()
private var indent: Int = 0
private var size: Int = 0
private val wrapped: MutableSet<Int> = mutableSetOf()
private var shouldAddIndent = false
fun generate(node: FormatNode) {
node(node, Wrap.DETECT)
}
private fun node(node: FormatNode, wrap: Wrap) {
when (node) {
is Empty -> {}
is Nodes -> node.nodes.forEach { node(it, wrap) }
is Group -> {
val width = node.nodes.sumOf { it.width(wrapped) }
val wrap =
if (size + width > MAX) {
wrapped += node.id
Wrap.ENABLED
} else {
Wrap.DETECT
}
node.nodes.forEach { node(it, wrap) }
}
is ForceWrap -> {
wrapped += node.id
val wrap = Wrap.ENABLED
node.nodes.forEach { node(it, wrap) }
}
is IfWrap -> {
if (wrapped.contains(node.id)) {
node(node.ifWrap, Wrap.ENABLED)
} else {
node(node.ifNotWrap, wrap)
}
}
is Text -> text(node.text)
is Line -> {
if (wrap.isEnabled()) {
newline()
}
}
is ForceLine -> newline()
is SpaceOrLine -> {
if (wrap.isEnabled()) {
newline()
} else {
text(" ")
}
}
is Space -> text(" ")
is Indent -> {
if (wrap.isEnabled() && node.nodes.isNotEmpty()) {
size += INDENT.length
indent++
node.nodes.forEach { node(it, wrap) }
indent--
} else {
node.nodes.forEach { node(it, wrap) }
}
}
is MultilineStringGroup -> {
val indentLength = indent * INDENT.length
val offset = (indentLength + 1) - node.endQuoteCol
var previousNewline = false
for ((i, child) in node.nodes.withIndex()) {
when {
child is ForceLine -> newline(shouldIndent = false) // don't indent
child is Text &&
previousNewline &&
child.text.isBlank() &&
child.text.length == indentLength &&
node.nodes[i + 1] is ForceLine -> {}
child is Text && previousNewline && offset != 0 -> text(reposition(child.text, offset))
else -> node(child, Wrap.DETECT) // always detect wrapping
}
previousNewline = child is ForceLine
}
}
}
}
private fun text(value: String) {
if (shouldAddIndent) {
repeat(times = indent) { buf.append(INDENT) }
shouldAddIndent = false
}
size += value.length
buf.append(value)
}
private fun newline(shouldIndent: Boolean = true) {
size = INDENT.length * indent
buf.append('\n')
shouldAddIndent = shouldIndent
}
private fun reposition(text: String, offset: Int): String {
return if (offset > 0) {
" ".repeat(offset) + text
} else {
text.drop(-offset)
}
}
override fun toString(): String {
return buf.toString()
}
companion object {
// max line length
const val MAX = 100
private const val INDENT = " "
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright © 2025 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.formatter
internal class NaturalOrderComparator(private val ignoreCase: Boolean = false) :
Comparator<String> {
override fun compare(s1: String, s2: String): Int {
var i = 0
var j = 0
while (i < s1.length && j < s2.length) {
val c1 = if (ignoreCase) s1[i].lowercaseChar() else s1[i]
val c2 = if (ignoreCase) s2[j].lowercaseChar() else s2[j]
if (c1.isDigit() && c2.isDigit()) {
val (num1, nextI) = getNumber(s1, i)
val (num2, nextJ) = getNumber(s2, j)
val numComparison = num1.compareTo(num2)
if (numComparison != 0) {
return numComparison
}
i = nextI
j = nextJ
} else {
val charComparison = c1.compareTo(c2)
if (charComparison != 0) {
return charComparison
}
i++
j++
}
}
return s1.length.compareTo(s2.length)
}
private fun getNumber(s: String, startIndex: Int): LongAndInt {
var i = startIndex
val start = i
while (i < s.length && s[i].isDigit()) {
i++
}
val numStr = s.substring(start, i)
val number = numStr.toLongOrNull() ?: 0L
return LongAndInt(number, i)
}
// use this instead of Pair to avoid boxing
private data class LongAndInt(val l: Long, var i: Int)
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright © 2025 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.formatter.ast
import org.pkl.formatter.Generator
enum class Wrap {
ENABLED,
DETECT;
fun isEnabled(): Boolean = this == ENABLED
}
sealed interface FormatNode {
fun width(wrapped: Set<Int>): Int =
when (this) {
is Nodes -> nodes.sumOf { it.width(wrapped) }
is Group -> nodes.sumOf { it.width(wrapped) }
is Indent -> nodes.sumOf { it.width(wrapped) }
is ForceWrap -> nodes.sumOf { it.width(wrapped + id) }
is IfWrap -> if (id in wrapped) ifWrap.width(wrapped) else ifNotWrap.width(wrapped)
is Text -> text.length
is SpaceOrLine,
is Space -> 1
is ForceLine,
is MultilineStringGroup -> Generator.MAX
else -> 0
}
}
data class Text(val text: String) : FormatNode
object Empty : FormatNode
object Line : FormatNode
object ForceLine : FormatNode
object SpaceOrLine : FormatNode
object Space : FormatNode
data class Indent(val nodes: List<FormatNode>) : FormatNode
data class Nodes(val nodes: List<FormatNode>) : FormatNode
data class Group(val id: Int, val nodes: List<FormatNode>) : FormatNode
data class ForceWrap(val id: Int, val nodes: List<FormatNode>) : FormatNode
data class MultilineStringGroup(val endQuoteCol: Int, val nodes: List<FormatNode>) : FormatNode
data class IfWrap(val id: Int, val ifWrap: FormatNode, val ifNotWrap: FormatNode) : FormatNode

View File

@@ -0,0 +1,10 @@
class Foo {
}
class Bar
{
qux = 1
}
class Baz { prop = 0; prop2 = 1 }

View File

@@ -0,0 +1,16 @@
function fun(a, b, c, d) = a
noTrailingCommas = fun(1, 2, 3, 4,)
trailingCommas = fun("loooooooooooooooooooongString", "loooooooooooooooongString", "looooooooooooooooongString", "notTooLong")
noTrailingCommaObjParams {
fooooooooooooooooooooooooooooooooo, baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar, baaaaaaaaaaaaaaaaaaaaaz ->
1 + 1
}
trailingCommaInLambdas = (fooooooooooooooooooooooooooooooooo, baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar, baaaaaaaaaaaaaaaaaaaaaz) -> 1
trailingCommaInConstraints: String(isSomethingSomethingSomething, isSomethingElse, isSomethingSomethingSomethingElse)
trailingCommaInTypeParameters: Mapping<SomethingSomethingSomethingSomething, SomethingSomething | SomethingElse>

View File

@@ -0,0 +1,31 @@
module comment.interleaved
local test: Int|String =
if (true)
1 // It's the same as "100%"
else
"8%"
foo: ( // some comment
* String(
// if dtstart is defined and a datetime, until must also be a datetime if defined
true
)
|Int
// trailing comment
)?
foo2: (// some comment
Int, // other comment
String
) ->Int
bar = ( // some comment
10 + 10
// another comment
)
bar2 = ( // some comment
foo, bar
// another comment
) -> foo + bar

View File

@@ -0,0 +1,8 @@
module dangling
/// Doc comment.
///
/// for this field
// sepearted by a stray comment
/// but continues here.
some: String

View File

@@ -0,0 +1,14 @@
//// line 1
///// line 2
foo = 1
/// line 1
///
///
/// line 2
bar = 1
///line 1
///line 2
baz = 1

View File

@@ -0,0 +1,25 @@
foo {
123123123123 + 123123123123 * 123123123123 - 123123123123 / 123123123123 + 123123123123 ** 123123123123
2
3
4
}
bar = clazz.superclass == null || clazz.superclass.reflectee == Module || clazz.superclass.reflectee == Typed
baz =
new Listing {
1
2
} |> mixin1 |> mixin2
qux =
(baz) {
1
2
} {
3
4
}
minus = superLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongVariable - 100000

View File

@@ -0,0 +1,5 @@
res = myList.map((it) -> it.partition).filter((it) -> someList.contains(it))
res2 = myList.map(lambda1).filter(lambda2)
res3 = myList.map((it) -> it.partition)

View File

@@ -0,0 +1,7 @@
foo =
if (someCondition) 10000
else
if (someOtherCondition) 20000
else
if (someAnotherCondition) 30000
else 4

View File

@@ -0,0 +1,11 @@
foo = let (vaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaariable = 10) 1 * 1
bar = let (someVariable = new Listing {
1
}) 1 * 1
baz =
let (someVariable = 10000000)
let (someOtherVariable = 2000000)
let (someAnotherVariable = 3000000) someVariable + someOtherVariable + someAnotherVariable

View File

@@ -0,0 +1,19 @@
// top level comment
import "@foo/Foo.pkl" as foo
import* "**.pkl"
import "pkl:math"
import "package://example.com/myPackage@1.0.0#/Qux.pkl"
import* "file:///tmp/*.pkl"
import "https://example.com/baz.pkl"
import "module2.pkl"
import "pkl:reflect"
import "..."
import* "@foo/**.pkl"
import "@bar/Bar.pkl"
import "Module12.pkl"
import "module11.pkl"
import "module1.pkl"

View File

@@ -0,0 +1,51 @@
foo = new {
bar = 1
}
class Foo {
baz: Int
}
bar =
new Listing {
1
2
}
baz =
(foo) {
2
3
}
qux =
(x, y) -> new Listing {
x
y
}
forGen = new Listing {
for (someVar in new Listing {
1
2
}) {
[someVar] = someVar
}
}
objParams: Listing<Int> =
new {
default {
key -> key + 1
}
}
objParams2: Listing<Int> = new {
default { someVeryLongParameter1, someVeryLongParameter2, someVeryLongParameter3, someVeryLongParameter4 ->
1
}
}
parenType: (ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongType(reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalylongConstraint))
functionType: (ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongType,ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongType2) -> String

View File

@@ -0,0 +1,64 @@
module
foo.bar.baz
amends
"bar.pkl"
import
"@foo/Foo.pkl"
as foo
local
open
class Bar {}
const
local
baz = 10
local
function
fun(x) =
x
const local prooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooperty: String = "foo"
local function function2(parameter1: Parameter1Type, parameter2: Parameter2Type, parameter3: Parameter3Type, parameter4: Parameter4Type): String = ""
local const function function3(parameter1: String|Int, parameter2: String|Int): Mapping<String|Int, String> =
new {}
prop = function2(loooooooooooooooooogParameter1, loooooooooooooooooogParameter2, loooooooooooooooooogParameter3, loooooooooooooooooogParameter4)
prop2: String
|Int
|Boolean
funcParam = fun(
(x, y) -> new Listing {
x
y
})
funcParam2 = aFun(foo, 10 * 10, anotherVariable, if (true) 100000 else 200000, (param1, param2) -> param1 * param2)
funcParam3 = aFun(foo, 10 * 10, anotherVariable, 200000, (param1, param2) -> param1 * param2)
funcParam4 = aFun(foo, 10 * 10, anotherVariable, if (true) 100000 else 200000, new Listing {
1
2
})
open local class SomeReallyInterestingClassName<in SomeInParameter, out SomeOutParameter> extends AnotherInterestingClassName {
foo: Int
}
local function resourceMapping(type): Mapping<String, unknown> =
new Mapping { default = (key) -> (type) { metadata { name = key } } }
local const function biiiiiiiiiiiiiiiiiiiiiiiiigFunction(param1: String, param2: String(!isBlank)): Boolean
local const function someFunction(param1: String, param2: String(!isBlank)): Boolean
local function render(currentIndent: String) =
"\(currentIndent)@\(identifier.render(currentIndent))" +
if (body == null) "" else " " + body.render(currentIndent)

View File

@@ -0,0 +1,20 @@
module reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly.loooooooooooooooooooooooooooooooog.naaaaaaaaaaaaaaaaaaaaaaaaaame
extends "reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongModule.pkl"
import "reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongModule.pkl" as foo
local open class LoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongName {}
local open class ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongName {}
const hidden loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogName = 99
const hidden reallyLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogName = 99
const local function looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogName(x: Int, y) = x + y
typealias Foo = LooooooooooooooooooooooooongTypeName|AnotherLooooooooooooooooooooooooongTypeName|OtherLooooooooooooooooooooooooongTypeName
bar: Boolean|Mapping<LooooooooooooooooooooooooooooongTypeName, AnotherLooooooooooooooooooooooooooooongTypeName>(loooooooooooooooooooogConstraint)
hidden foobar: LongType(someVeryyyyyyyloooong, requirements.might.be.even_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooonger)

View File

@@ -0,0 +1,18 @@
module reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly.loooooooooooooooooooooooooooooooog.naaaaaaaaaaaaaaaaaaaaaaaaaaaaame
extends "reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongModule.pkl"
local reallyLongVariableName = true
local anotherReallyLongName = 1
local evenLongerVariableName = 2
/// this property has a reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly long doc comment
property = if (reallyLongVariableName) anotherReallyLongName else evenLongerVariableName + anotherReallyLongName
longString = "reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly long string"
shortProperty = 1
reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongVariableName = 10
reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongVariableName2 =
if (reallyLongVariableName) anotherReallyLongName else evenLongerVariableName + anotherReallyLongName

View File

@@ -0,0 +1,4 @@
foo = Map(1000, "some random string", 20000, "another random string", 30000, "yet another random string")
incorrect = Map("This has", 1000000, "an incorrect number", 2000000, "of parameters", 30000000, "passed to Map")

View File

@@ -0,0 +1,3 @@
hidden const foo = 1
open local class Foo {}

View File

@@ -0,0 +1,13 @@
// comment
// one
/// doc comment
open
module
foo
.bar
amends
"baz.pkl"

View File

@@ -0,0 +1,29 @@
foo = """
asd \(new {
bar = 1
}) asd
"""
bar = """
line 1
line 2
line3
"""
baz = """
\n
\(bar)
line
\u{123}
"""
// remove unneeded spaces
qux = """
foo
bar
baz
\(foo)
"""

View File

@@ -0,0 +1,22 @@
foo: Listing<Int> = new {1 2 3; 4;5 6 7}
bar: Listing<Int> = new { 1 2
3
4
}
lineIsTooBig: Listing<Int> = new { 999999; 1000000; 1000001; 1000002; 1000003; 1000004; 1000005; 1000006; 1000007; 1000008; 1000009 }
lineIsTooBig2 { 999999; 1000000; 1000001; 1000002; 1000003; 1000004; 1000005; 1000006; 1000007; 1000008; 1000009; 1000010 }
baz = new Dynamic {
1 2
3 4
["foo"] = 3; bar = 30
baz = true
}
qux = new Dynamic {1 2 3 prop="prop"; [0]=9}

View File

@@ -0,0 +1,12 @@
/// Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong doc comment
@Annotation { looooooooooooooooooooooooooooooooooooooooooooooooooongVariableName = 10 }
typealias Foo = String
/// Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong doc comment
@Annotation { looooooooooooooooooooooooooooooooooooooooooooooooooongVariableName = 10 }
foo = "should fit in a single line"
/// Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong doc comment
@Annotation { looooooooooooooooooooooooooooooooooooooooooooooooooongVariableName = 10 }
function bar(x): String = "result: \(x)"

View File

@@ -0,0 +1,23 @@
import"foo.pkl"
bar=new Listing < Int > ( !isEmpty ){1;2}//a bar
typealias Typealias=(String,Int)->Boolean
///a baz
///returns its parameter
baz = (x,y,z)->x+y+z
function fun ( x:Int ? ,b :Boolean )= if(b)/***return x**/x else x+bar [0 ]
prop = trace (1) + super . foo + module .foo
prop2 {
for(x in List(1)) {
when(x == 1){
x
}
}
}
choices: "foo" | * "bar" | String

View File

@@ -0,0 +1,6 @@
typealias
Foo
=
String
typealias VeryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongTypeAlias = String(!isEmpty)

View File

@@ -0,0 +1,9 @@
foo {
when (true)
{
bar = 1
}
else {
bar = 2
}
}

View File

@@ -0,0 +1,11 @@
class Foo {}
class Bar {
qux = 1
}
class Baz {
prop = 0
prop2 = 1
}

View File

@@ -0,0 +1,35 @@
function fun(a, b, c, d) = a
noTrailingCommas = fun(1, 2, 3, 4)
trailingCommas =
fun(
"loooooooooooooooooooongString",
"loooooooooooooooongString",
"looooooooooooooooongString",
"notTooLong",
)
noTrailingCommaObjParams {
fooooooooooooooooooooooooooooooooo,
baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar,
baaaaaaaaaaaaaaaaaaaaaz ->
1 + 1
}
trailingCommaInLambdas = (
fooooooooooooooooooooooooooooooooo,
baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar,
baaaaaaaaaaaaaaaaaaaaaz,
) -> 1
trailingCommaInConstraints: String(
isSomethingSomethingSomething,
isSomethingElse,
isSomethingSomethingSomethingElse,
)
trailingCommaInTypeParameters: Mapping<
SomethingSomethingSomethingSomething,
SomethingSomething | SomethingElse,
>

View File

@@ -0,0 +1,34 @@
module comment.interleaved
local test: Int | String =
if (true)
1 // It's the same as "100%"
else
"8%"
foo: ( // some comment
*String(
// if dtstart is defined and a datetime, until must also be a datetime if defined
true,
)
| Int
// trailing comment
)?
foo2: ( // some comment
Int, // other comment
String,
) ->
Int
bar =
( // some comment
10 + 10
// another comment
)
bar2 = ( // some comment
foo,
bar
// another comment
) -> foo + bar

View File

@@ -0,0 +1,8 @@
module dangling
/// Doc comment.
///
/// for this field
// sepearted by a stray comment
/// but continues here.
some: String

View File

@@ -0,0 +1,13 @@
/// / line 1
/// // line 2
foo = 1
/// line 1
///
///
/// line 2
bar = 1
/// line 1
/// line 2
baz = 1

View File

@@ -0,0 +1,34 @@
foo {
123123123123
+ 123123123123 * 123123123123 -
123123123123 / 123123123123
+ 123123123123 ** 123123123123
2
3
4
}
bar =
clazz.superclass == null
|| clazz.superclass.reflectee == Module
|| clazz.superclass.reflectee == Typed
baz =
new Listing {
1
2
}
|> mixin1
|> mixin2
qux = (baz) {
1
2
} {
3
4
}
minus =
superLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongVariable -
100000

View File

@@ -0,0 +1,8 @@
res =
myList
.map((it) -> it.partition)
.filter((it) -> someList.contains(it))
res2 = myList.map(lambda1).filter(lambda2)
res3 = myList.map((it) -> it.partition)

View File

@@ -0,0 +1,9 @@
foo =
if (someCondition)
10000
else if (someOtherCondition)
20000
else if (someAnotherCondition)
30000
else
4

View File

@@ -0,0 +1,18 @@
foo =
let (vaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaariable =
10
)
1 * 1
bar =
let (someVariable = new Listing {
1
}
)
1 * 1
baz =
let (someVariable = 10000000)
let (someOtherVariable = 2000000)
let (someAnotherVariable = 3000000)
someVariable + someOtherVariable + someAnotherVariable

View File

@@ -0,0 +1,21 @@
// top level comment
import "https://example.com/baz.pkl"
import "package://example.com/myPackage@1.0.0#/Qux.pkl"
import "pkl:math"
import "pkl:reflect"
import "@bar/Bar.pkl"
import "@foo/Foo.pkl" as foo
import "..."
import "module1.pkl"
import "module2.pkl"
import "module11.pkl"
import "Module12.pkl"
import* "file:///tmp/*.pkl"
import* "@foo/**.pkl"
import* "**.pkl"

View File

@@ -0,0 +1,61 @@
foo = new {
bar = 1
}
class Foo {
baz: Int
}
bar = new Listing {
1
2
}
baz = (foo) {
2
3
}
qux = (x, y) -> new Listing {
x
y
}
forGen = new Listing {
for (
someVar in new Listing {
1
2
}
) {
[someVar] = someVar
}
}
objParams: Listing<Int> = new {
default { key ->
key + 1
}
}
objParams2: Listing<Int> = new {
default {
someVeryLongParameter1,
someVeryLongParameter2,
someVeryLongParameter3,
someVeryLongParameter4 ->
1
}
}
parenType: (
ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongType(
reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalylongConstraint,
)
)
functionType: (
ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongType,
ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongType2,
) ->
String

View File

@@ -0,0 +1,75 @@
module foo.bar.baz
amends "bar.pkl"
import "@foo/Foo.pkl" as foo
open local class Bar {}
local const baz = 10
local function fun(x) = x
local const prooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooperty: String =
"foo"
local function function2(
parameter1: Parameter1Type,
parameter2: Parameter2Type,
parameter3: Parameter3Type,
parameter4: Parameter4Type,
): String = ""
local const function function3(
parameter1: String | Int,
parameter2: String | Int,
): Mapping<String | Int, String> = new {}
prop =
function2(
loooooooooooooooooogParameter1,
loooooooooooooooooogParameter2,
loooooooooooooooooogParameter3,
loooooooooooooooooogParameter4,
)
prop2: String | Int | Boolean
funcParam =
fun((x, y) -> new Listing {
x
y
})
funcParam2 =
aFun(foo, 10 * 10, anotherVariable, if (true) 100000 else 200000, (param1, param2) ->
param1 * param2
)
funcParam3 = aFun(foo, 10 * 10, anotherVariable, 200000, (param1, param2) -> param1 * param2)
funcParam4 =
aFun(foo, 10 * 10, anotherVariable, if (true) 100000 else 200000, new Listing {
1
2
})
open local class SomeReallyInterestingClassName<in SomeInParameter, out SomeOutParameter>
extends AnotherInterestingClassName {
foo: Int
}
local function resourceMapping(type): Mapping<String, unknown> = new Mapping {
default = (key) -> (type) { metadata { name = key } }
}
local const function biiiiiiiiiiiiiiiiiiiiiiiiigFunction(
param1: String,
param2: String(!isBlank),
): Boolean
local const function someFunction(param1: String, param2: String(!isBlank)): Boolean
local function render(currentIndent: String) =
"\(currentIndent)@\(identifier.render(currentIndent))"
+ if (body == null) "" else " " + body.render(currentIndent)

View File

@@ -0,0 +1,42 @@
module
reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly
.loooooooooooooooooooooooooooooooog
.naaaaaaaaaaaaaaaaaaaaaaaaaame
extends
"reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongModule.pkl"
import
"reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongModule.pkl"
as foo
open local class LoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongName {}
open local class ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongName {}
hidden const loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogName =
99
hidden const reallyLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogName =
99
local const function looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogName(
x: Int,
y,
) = x + y
typealias Foo =
LooooooooooooooooooooooooongTypeName
| AnotherLooooooooooooooooooooooooongTypeName
| OtherLooooooooooooooooooooooooongTypeName
bar: Boolean
| Mapping<
LooooooooooooooooooooooooooooongTypeName,
AnotherLooooooooooooooooooooooooooooongTypeName,
>(loooooooooooooooooooogConstraint)
hidden foobar: LongType(
someVeryyyyyyyloooong,
requirements.might.be.even_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooonger,
)

View File

@@ -0,0 +1,32 @@
module
reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly
.loooooooooooooooooooooooooooooooog
.naaaaaaaaaaaaaaaaaaaaaaaaaaaaame
extends
"reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongModule.pkl"
local reallyLongVariableName = true
local anotherReallyLongName = 1
local evenLongerVariableName = 2
/// this property has a reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly long doc comment
property =
if (reallyLongVariableName)
anotherReallyLongName
else
evenLongerVariableName + anotherReallyLongName
longString =
"reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaly long string"
shortProperty = 1
reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongVariableName =
10
reaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongVariableName2 =
if (reallyLongVariableName)
anotherReallyLongName
else
evenLongerVariableName + anotherReallyLongName

View File

@@ -0,0 +1,14 @@
foo =
Map(
1000, "some random string",
20000, "another random string",
30000, "yet another random string",
)
incorrect =
Map(
"This has", 1000000,
"an incorrect number", 2000000,
"of parameters", 30000000,
"passed to Map",
)

View File

@@ -0,0 +1,3 @@
hidden const foo = 1
open local class Foo {}

View File

@@ -0,0 +1,7 @@
// comment
// one
/// doc comment
open module foo.bar
amends "baz.pkl"

View File

@@ -0,0 +1,33 @@
foo =
"""
asd \(new {
bar = 1
}) asd
"""
bar =
"""
line 1
line 2
line3
"""
baz =
"""
\n
\(bar)
line
\u{123}
"""
// remove unneeded spaces
qux =
"""
foo
bar
baz
\(foo)
"""

View File

@@ -0,0 +1,51 @@
foo: Listing<Int> = new { 1; 2; 3; 4; 5; 6; 7 }
bar: Listing<Int> = new {
1
2
3
4
}
lineIsTooBig: Listing<Int> = new {
999999
1000000
1000001
1000002
1000003
1000004
1000005
1000006
1000007
1000008
1000009
}
lineIsTooBig2 {
999999
1000000
1000001
1000002
1000003
1000004
1000005
1000006
1000007
1000008
1000009
1000010
}
baz = new Dynamic {
1
2
3
4
["foo"] = 3
bar = 30
baz = true
}
qux = new Dynamic { 1; 2; 3; prop = "prop"; [0] = 9 }

View File

@@ -0,0 +1,11 @@
/// Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong doc comment
@Annotation { looooooooooooooooooooooooooooooooooooooooooooooooooongVariableName = 10 }
typealias Foo = String
/// Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong doc comment
@Annotation { looooooooooooooooooooooooooooooooooooooooooooooooooongVariableName = 10 }
foo = "should fit in a single line"
/// Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong doc comment
@Annotation { looooooooooooooooooooooooooooooooooooooooooooooooooongVariableName = 10 }
function bar(x): String = "result: \(x)"

View File

@@ -0,0 +1,23 @@
import "foo.pkl"
bar = new Listing<Int>(!isEmpty) { 1; 2 } //a bar
typealias Typealias = (String, Int) -> Boolean
/// a baz
/// returns its parameter
baz = (x, y, z) -> x + y + z
function fun(x: Int?, b: Boolean) = if (b) /***return x**/ x else x + bar[0]
prop = trace(1) + super.foo + module.foo
prop2 {
for (x in List(1)) {
when (x == 1) {
x
}
}
}
choices: "foo" | *"bar" | String

View File

@@ -0,0 +1,4 @@
typealias Foo = String
typealias VeryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongTypeAlias =
String(!isEmpty)

View File

@@ -0,0 +1,7 @@
foo {
when (true) {
bar = 1
} else {
bar = 2
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright © 2024-2025 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.formatter
import org.junit.platform.commons.annotation.Testable
@Testable class FormatterSnippetTests

View File

@@ -0,0 +1,73 @@
/*
* Copyright © 2025 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.formatter
import java.nio.file.Path
import kotlin.io.path.isRegularFile
import kotlin.reflect.KClass
import org.pkl.commons.test.InputOutputTestEngine
import org.pkl.parser.ParserError
abstract class AbstractFormatterSnippetTestsEngine : InputOutputTestEngine() {
private val snippetsDir: Path =
rootProjectDir.resolve("pkl-formatter/src/test/files/FormatterSnippetTests")
private val expectedOutputDir: Path = snippetsDir.resolve("output")
/** Convenience for development; this selects which snippet test(s) to run. */
// language=regexp
internal val selection: String = ""
override val includedTests: List<Regex> = listOf(Regex(".*$selection\\.pkl"))
override val inputDir: Path = snippetsDir.resolve("input")
override val isInputFile: (Path) -> Boolean = { it.isRegularFile() }
override fun expectedOutputFileFor(inputFile: Path): Path {
val relativePath = relativize(inputFile, inputDir).toString()
return expectedOutputDir.resolve(relativePath)
}
companion object {
private fun relativize(path: Path, base: Path): Path {
if (System.getProperty("os.name").contains("Windows")) {
if (path.isAbsolute && base.isAbsolute && (path.root != base.root)) {
return path
}
}
return base.relativize(path)
}
}
}
class FormatterSnippetTestsEngine : AbstractFormatterSnippetTestsEngine() {
override val testClass: KClass<*> = FormatterSnippetTests::class
override fun generateOutputFor(inputFile: Path): Pair<Boolean, String> {
val formatter = Formatter()
val (success, output) =
try {
val res = formatter.format(inputFile)
true to res
} catch (_: ParserError) {
false to ""
}
return success to output
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright © 2025 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.formatter
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.pkl.parser.GenericParserError
class FormatterTest {
@Test
fun `multiline string - wrong start quote`() {
val ex =
assertThrows<GenericParserError> {
format(
"""
foo = ""${'"'}line1
line 2
""${'"'}
"""
)
}
assertThat(ex.message).contains("The content of a multi-line string must begin on a new line")
}
@Test
fun `multiline string - wrong end quote`() {
val ex =
assertThrows<GenericParserError> {
format(
"""
foo = ""${'"'}
line1
line 2""${'"'}
"""
)
}
assertThat(ex.message)
.contains("The closing delimiter of a multi-line string must begin on a new line")
}
@Test
fun `multiline string - wrong indentation`() {
val ex =
assertThrows<GenericParserError> {
format(
"""
foo = ""${'"'}
line1
line 2
""${'"'}
"""
)
}
assertThat(ex.message)
.contains("Line must match or exceed indentation of the String's last line.")
}
private fun format(code: String): String {
return Formatter().format(code.trimIndent())
}
}

View File

@@ -0,0 +1 @@
org.pkl.formatter.FormatterSnippetTestsEngine

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
/*
* Copyright © 2025 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.parser;
import org.pkl.parser.syntax.generic.FullSpan;
public class GenericParserError extends RuntimeException {
private final FullSpan span;
public GenericParserError(String msg, FullSpan span) {
super(msg);
this.span = span;
}
public FullSpan getSpan() {
return span;
}
@Override
public String toString() {
return getMessage() + " at " + span;
}
}

View File

@@ -17,6 +17,7 @@ package org.pkl.parser;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Deque; import java.util.Deque;
import org.pkl.parser.syntax.generic.FullSpan;
import org.pkl.parser.util.ErrorMessages; import org.pkl.parser.util.ErrorMessages;
public class Lexer { public class Lexer {
@@ -25,6 +26,10 @@ public class Lexer {
private final int size; private final int size;
protected int cursor = 0; protected int cursor = 0;
protected int sCursor = 0; protected int sCursor = 0;
private int line = 1;
private int sLine = 1;
private int col = 1;
private int sCol = 1;
private char lookahead; private char lookahead;
private State state = State.DEFAULT; private State state = State.DEFAULT;
private final Deque<InterpolationScope> interpolationStack = new ArrayDeque<>(); private final Deque<InterpolationScope> interpolationStack = new ArrayDeque<>();
@@ -50,6 +55,11 @@ public class Lexer {
return new Span(sCursor, cursor - sCursor); return new Span(sCursor, cursor - sCursor);
} }
// The full span of the last lexed token
public FullSpan fullSpan() {
return new FullSpan(sCursor, cursor - sCursor, sLine, sCol, line, col);
}
// The text of the last lexed token // The text of the last lexed token
public String text() { public String text() {
return new String(source, sCursor, cursor - sCursor); return new String(source, sCursor, cursor - sCursor);
@@ -65,6 +75,8 @@ public class Lexer {
public Token next() { public Token next() {
sCursor = cursor; sCursor = cursor;
sLine = line;
sCol = col;
newLinesBetween = 0; newLinesBetween = 0;
return switch (state) { return switch (state) {
case DEFAULT -> nextDefault(); case DEFAULT -> nextDefault();
@@ -79,7 +91,9 @@ public class Lexer {
sCursor = cursor; sCursor = cursor;
if (ch == '\n') { if (ch == '\n') {
newLinesBetween++; newLinesBetween++;
sLine = line;
} }
sCol = col;
ch = nextChar(); ch = nextChar();
} }
return switch (ch) { return switch (ch) {
@@ -678,15 +692,23 @@ public class Lexer {
} else { } else {
lookahead = source[cursor]; lookahead = source[cursor];
} }
if (tmp == '\n') {
line++;
col = 1;
} else {
col++;
}
return tmp; return tmp;
} }
private void backup() { private void backup() {
lookahead = source[--cursor]; lookahead = source[--cursor];
col--;
} }
private void backup(int amount) { private void backup(int amount) {
cursor -= amount; cursor -= amount;
col -= amount;
lookahead = source[cursor]; lookahead = source[cursor];
} }

View File

@@ -18,7 +18,6 @@ package org.pkl.parser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.pkl.parser.syntax.Annotation; import org.pkl.parser.syntax.Annotation;
@@ -1809,16 +1808,13 @@ public class Parser {
private FullToken forceNext() { private FullToken forceNext() {
var tk = lexer.next(); var tk = lexer.next();
precededBySemicolon = false; precededBySemicolon = false;
while (AFFIXES.contains(tk)) { while (tk.isAffix()) {
precededBySemicolon = precededBySemicolon || tk == Token.SEMICOLON; precededBySemicolon = precededBySemicolon || tk == Token.SEMICOLON;
tk = lexer.next(); tk = lexer.next();
} }
return new FullToken(tk, lexer.span(), lexer.newLinesBetween); return new FullToken(tk, lexer.span(), lexer.newLinesBetween);
} }
private static final EnumSet<Token> AFFIXES =
EnumSet.of(Token.LINE_COMMENT, Token.BLOCK_COMMENT, Token.SEMICOLON);
// Like next, but don't ignore comments // Like next, but don't ignore comments
private FullToken nextComment() { private FullToken nextComment() {
prev = _lookahead; prev = _lookahead;

View File

@@ -191,6 +191,13 @@ public enum Token {
}; };
} }
public boolean isAffix() {
return switch (this) {
case LINE_COMMENT, BLOCK_COMMENT, SEMICOLON -> true;
default -> false;
};
}
public String text() { public String text() {
if (this == UNDERSCORE) { if (this == UNDERSCORE) {
return "_"; return "_";

View File

@@ -35,8 +35,10 @@ public enum Operator {
INT_DIV(9, true), INT_DIV(9, true),
MOD(9, true), MOD(9, true),
POW(10, false), POW(10, false),
DOT(11, true), NON_NULL(16, true),
QDOT(11, true); SUBSCRIPT(18, true),
DOT(20, true),
QDOT(20, true);
private final int prec; private final int prec;
private final boolean isLeftAssoc; private final boolean isLeftAssoc;
@@ -53,4 +55,33 @@ public enum Operator {
public boolean isLeftAssoc() { public boolean isLeftAssoc() {
return isLeftAssoc; return isLeftAssoc;
} }
public static Operator byName(String name) {
return switch (name) {
case "??" -> NULL_COALESCE;
case "|>" -> PIPE;
case "||" -> OR;
case "&&" -> AND;
case "==" -> EQ_EQ;
case "!=" -> NOT_EQ;
case "is" -> IS;
case "as" -> AS;
case "<" -> LT;
case "<=" -> LTE;
case ">" -> GT;
case ">=" -> GTE;
case "+" -> PLUS;
case "-" -> MINUS;
case "*" -> MULT;
case "/" -> DIV;
case "~/" -> INT_DIV;
case "%" -> MOD;
case "**" -> POW;
case "!!" -> NON_NULL;
case "[" -> SUBSCRIPT;
case "." -> DOT;
case "?." -> QDOT;
default -> throw new RuntimeException("Unknown operator: " + name);
};
}
} }

View File

@@ -0,0 +1,49 @@
/*
* Copyright © 2025 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.parser.syntax.generic;
import org.pkl.parser.Span;
public record FullSpan(
int charIndex, int length, int lineBegin, int colBegin, int lineEnd, int colEnd) {
public FullSpan endWith(FullSpan end) {
return new FullSpan(
charIndex,
end.charIndex - charIndex + end.length,
lineBegin,
colBegin,
end.lineEnd,
end.colEnd);
}
public Span toSpan() {
return new Span(charIndex, length);
}
public boolean sameLine(FullSpan other) {
return lineEnd == other.lineBegin;
}
public FullSpan stopSpan() {
return new FullSpan(charIndex + length - 1, 1, lineEnd, colEnd, lineEnd, colEnd);
}
@Override
public String toString() {
return "(%d:%d - %d:%d)".formatted(lineBegin, colBegin, lineEnd, colEnd);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright © 2025 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.parser.syntax.generic;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.pkl.parser.util.Nullable;
public class Node {
public final List<Node> children;
public final FullSpan span;
public final NodeType type;
private @Nullable String text;
public Node(NodeType type, FullSpan span) {
this(type, span, Collections.emptyList());
}
public Node(NodeType type, FullSpan span, List<Node> children) {
this.type = type;
this.span = span;
this.children = Collections.unmodifiableList(children);
}
public Node(NodeType type, List<Node> children) {
this.type = type;
if (children.isEmpty()) throw new RuntimeException("No children or span given for node");
var end = children.get(children.size() - 1).span;
this.span = children.get(0).span.endWith(end);
this.children = Collections.unmodifiableList(children);
}
public String text(char[] source) {
if (text == null) {
text = new String(source, span.charIndex(), span.length());
}
return text;
}
/** Returns the first child of type {@code type} or {@code null}. */
public @Nullable Node findChildByType(NodeType type) {
for (var child : children) {
if (child.type == type) return child;
}
return null;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Node node = (Node) o;
return Objects.equals(children, node.children)
&& Objects.equals(span, node.span)
&& Objects.equals(type, node.type);
}
@Override
public int hashCode() {
return Objects.hash(children, span, type);
}
@Override
public String toString() {
return "Node{type='" + type + "', span=" + span + ", children=" + children + '}';
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright © 2025 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.parser.syntax.generic;
public enum NodeType {
TERMINAL,
SHEBANG,
// affixes,
LINE_COMMENT(NodeKind.AFFIX),
BLOCK_COMMENT(NodeKind.AFFIX),
SEMICOLON(NodeKind.AFFIX),
MODULE,
DOC_COMMENT,
DOC_COMMENT_LINE,
MODIFIER,
MODIFIER_LIST,
AMENDS_CLAUSE,
EXTENDS_CLAUSE,
MODULE_DECLARATION,
MODULE_DEFINITION,
ANNOTATION,
IDENTIFIER,
QUALIFIED_IDENTIFIER,
IMPORT,
IMPORT_ALIAS,
IMPORT_LIST,
TYPEALIAS,
TYPEALIAS_HEADER,
TYPEALIAS_BODY,
CLASS,
CLASS_HEADER,
CLASS_HEADER_EXTENDS,
CLASS_BODY,
CLASS_BODY_ELEMENTS,
CLASS_METHOD,
CLASS_METHOD_HEADER,
CLASS_METHOD_BODY,
CLASS_PROPERTY,
CLASS_PROPERTY_HEADER,
CLASS_PROPERTY_HEADER_BEGIN,
CLASS_PROPERTY_BODY,
OBJECT_BODY,
OBJECT_MEMBER_LIST,
PARAMETER,
TYPE_ANNOTATION,
PARAMETER_LIST,
PARAMETER_LIST_ELEMENTS,
TYPE_PARAMETER_LIST,
TYPE_PARAMETER_LIST_ELEMENTS,
ARGUMENT_LIST,
ARGUMENT_LIST_ELEMENTS,
TYPE_ARGUMENT_LIST,
TYPE_ARGUMENT_LIST_ELEMENTS,
OBJECT_PARAMETER_LIST,
TYPE_PARAMETER,
STRING_CONSTANT,
OPERATOR,
STRING_NEWLINE,
STRING_ESCAPE,
// members
OBJECT_ELEMENT,
OBJECT_PROPERTY,
OBJECT_PROPERTY_HEADER,
OBJECT_PROPERTY_HEADER_BEGIN,
OBJECT_PROPERTY_BODY,
OBJECT_METHOD,
MEMBER_PREDICATE,
OBJECT_ENTRY,
OBJECT_ENTRY_HEADER,
OBJECT_SPREAD,
WHEN_GENERATOR,
WHEN_GENERATOR_HEADER,
FOR_GENERATOR,
FOR_GENERATOR_HEADER,
FOR_GENERATOR_HEADER_DEFINITION,
FOR_GENERATOR_HEADER_DEFINITION_HEADER,
// expressions
THIS_EXPR(NodeKind.EXPR),
OUTER_EXPR(NodeKind.EXPR),
MODULE_EXPR(NodeKind.EXPR),
NULL_EXPR(NodeKind.EXPR),
THROW_EXPR(NodeKind.EXPR),
TRACE_EXPR(NodeKind.EXPR),
IMPORT_EXPR(NodeKind.EXPR),
READ_EXPR(NodeKind.EXPR),
NEW_EXPR(NodeKind.EXPR),
NEW_HEADER,
UNARY_MINUS_EXPR(NodeKind.EXPR),
LOGICAL_NOT_EXPR(NodeKind.EXPR),
FUNCTION_LITERAL_EXPR(NodeKind.EXPR),
FUNCTION_LITERAL_BODY,
PARENTHESIZED_EXPR(NodeKind.EXPR),
PARENTHESIZED_EXPR_ELEMENTS,
SUPER_SUBSCRIPT_EXPR(NodeKind.EXPR),
SUPER_ACCESS_EXPR(NodeKind.EXPR),
SUBSCRIPT_EXPR(NodeKind.EXPR),
IF_EXPR(NodeKind.EXPR),
IF_HEADER,
IF_CONDITION,
IF_CONDITION_EXPR,
IF_THEN_EXPR,
IF_ELSE_EXPR,
LET_EXPR(NodeKind.EXPR),
LET_PARAMETER_DEFINITION,
LET_PARAMETER,
BOOL_LITERAL_EXPR(NodeKind.EXPR),
INT_LITERAL_EXPR(NodeKind.EXPR),
FLOAT_LITERAL_EXPR(NodeKind.EXPR),
SINGLE_LINE_STRING_LITERAL_EXPR(NodeKind.EXPR),
MULTI_LINE_STRING_LITERAL_EXPR(NodeKind.EXPR),
UNQUALIFIED_ACCESS_EXPR(NodeKind.EXPR),
NON_NULL_EXPR(NodeKind.EXPR),
AMENDS_EXPR(NodeKind.EXPR),
BINARY_OP_EXPR(NodeKind.EXPR),
// types
UNKNOWN_TYPE(NodeKind.TYPE),
NOTHING_TYPE(NodeKind.TYPE),
MODULE_TYPE(NodeKind.TYPE),
UNION_TYPE(NodeKind.TYPE),
FUNCTION_TYPE(NodeKind.TYPE),
FUNCTION_TYPE_PARAMETERS,
PARENTHESIZED_TYPE(NodeKind.TYPE),
PARENTHESIZED_TYPE_ELEMENTS,
DECLARED_TYPE(NodeKind.TYPE),
NULLABLE_TYPE(NodeKind.TYPE),
STRING_CONSTANT_TYPE(NodeKind.TYPE),
CONSTRAINED_TYPE(NodeKind.TYPE),
CONSTRAINED_TYPE_CONSTRAINT,
CONSTRAINED_TYPE_ELEMENTS;
private final NodeKind kind;
NodeType() {
this.kind = NodeKind.NONE;
}
NodeType(NodeKind kind) {
this.kind = kind;
}
public boolean isAffix() {
return kind == NodeKind.AFFIX;
}
public boolean isExpression() {
return kind == NodeKind.EXPR;
}
public boolean isType() {
return kind == NodeKind.TYPE;
}
private enum NodeKind {
TYPE,
EXPR,
AFFIX,
NONE;
}
}

View File

@@ -0,0 +1,4 @@
@NonnullByDefault
package org.pkl.parser.syntax.generic;
import org.pkl.parser.util.NonnullByDefault;

View File

@@ -0,0 +1,261 @@
/*
* Copyright © 2025 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.parser
import java.util.EnumSet
import org.pkl.parser.syntax.generic.Node
import org.pkl.parser.syntax.generic.NodeType
class GenericSexpRenderer(code: String) {
private var tab = ""
private var buf = StringBuilder()
private val source = code.toCharArray()
fun render(node: Node): String {
innerRender(node)
return buf.toString()
}
private fun innerRender(node: Node) {
if (node.type == NodeType.UNION_TYPE) {
renderUnionType(node)
return
}
if (node.type == NodeType.BINARY_OP_EXPR && binopName(node).endsWith("ualifiedAccessExpr")) {
renderQualifiedAccess(node)
return
}
doRender(name(node), collectChildren(node))
}
private fun doRender(name: String, children: List<Node>) {
buf.append(tab)
buf.append("(")
buf.append(name)
val oldTab = increaseTab()
for (child in children) {
buf.append('\n')
innerRender(child)
}
tab = oldTab
buf.append(')')
}
private fun renderUnionType(node: Node) {
buf.append(tab)
buf.append("(")
buf.append(name(node))
val oldTab = increaseTab()
var previousTerminal: Node? = null
for (child in node.children) {
if (child.type == NodeType.TERMINAL) previousTerminal = child
if (child.type in IGNORED_CHILDREN) continue
buf.append('\n')
if (previousTerminal != null && previousTerminal.text(source) == "*") {
previousTerminal = null
renderDefaultUnionType(child)
} else {
innerRender(child)
}
}
tab = oldTab
buf.append(')')
}
private fun renderQualifiedAccess(node: Node) {
var children = node.children
if (children.last().type == NodeType.UNQUALIFIED_ACCESS_EXPR) {
children = children.dropLast(1) + collectChildren(children.last())
}
val toRender = mutableListOf<Node>()
for (child in children) {
if (child.type in IGNORED_CHILDREN || child.type == NodeType.OPERATOR) continue
toRender += child
}
doRender(name(node), toRender)
}
private fun renderDefaultUnionType(node: Node) {
buf.append(tab)
buf.append("(defaultUnionType\n")
val oldTab = increaseTab()
innerRender(node)
tab = oldTab
buf.append(')')
}
private fun collectChildren(node: Node): List<Node> =
when (node.type) {
NodeType.MULTI_LINE_STRING_LITERAL_EXPR ->
node.children.filter { it.type !in IGNORED_CHILDREN && !it.type.isStringData() }
NodeType.SINGLE_LINE_STRING_LITERAL_EXPR -> {
val children = node.children.filter { it.type !in IGNORED_CHILDREN }
val res = mutableListOf<Node>()
var prev: Node? = null
for (child in children) {
val inARow = child.type.isStringData() && (prev != null && prev.type.isStringData())
if (!inARow) {
res += child
}
prev = child
}
res
}
NodeType.DOC_COMMENT -> listOf()
else -> {
val nodes = mutableListOf<Node>()
for (child in node.children) {
if (child.type in IGNORED_CHILDREN) continue
if (child.type in UNPACK_CHILDREN) {
nodes += collectChildren(child)
} else {
nodes += child
}
}
nodes
}
}
private fun NodeType.isStringData(): Boolean =
this == NodeType.STRING_CONSTANT || this == NodeType.STRING_ESCAPE
private fun name(node: Node): String =
when (node.type) {
NodeType.MODULE_DECLARATION -> "moduleHeader"
NodeType.IMPORT -> importName(node, isExpr = false)
NodeType.IMPORT_EXPR -> importName(node, isExpr = true)
NodeType.BINARY_OP_EXPR -> binopName(node)
NodeType.CLASS -> "clazz"
NodeType.EXTENDS_CLAUSE,
NodeType.AMENDS_CLAUSE -> "extendsOrAmendsClause"
NodeType.TYPEALIAS -> "typeAlias"
NodeType.STRING_ESCAPE -> "stringConstant"
NodeType.READ_EXPR -> {
val terminal = node.children.find { it.type == NodeType.TERMINAL }!!.text(source)
when (terminal) {
"read*" -> "readGlobExpr"
"read?" -> "readNullExpr"
else -> "readExpr"
}
}
else -> {
val names = node.type.name.split('_').map { it.lowercase() }
if (names.size > 1) {
val capitalized = names.drop(1).map { n -> n.replaceFirstChar { it.titlecase() } }
(listOf(names[0]) + capitalized).joinToString("")
} else names[0]
}
}
private fun importName(node: Node, isExpr: Boolean): String {
val terminal = node.children.find { it.type == NodeType.TERMINAL }!!.text(source)
val suffix = if (isExpr) "Expr" else "Clause"
return if (terminal == "import*") "importGlob$suffix" else "import$suffix"
}
private fun binopName(node: Node): String {
val op = node.children.find { it.type == NodeType.OPERATOR }!!.text(source)
return when (op) {
"**" -> "exponentiationExpr"
"*",
"/",
"~/",
"%" -> "multiplicativeExpr"
"+",
"-" -> "additiveExpr"
">",
">=",
"<",
"<=" -> "comparisonExpr"
"is" -> "typeCheckExpr"
"as" -> "typeCastExpr"
"==",
"!=" -> "equalityExpr"
"&&" -> "logicalAndExpr"
"||" -> "logicalOrExpr"
"|>" -> "pipeExpr"
"??" -> "nullCoalesceExpr"
"." -> "qualifiedAccessExpr"
"?." -> "nullableQualifiedAccessExpr"
else -> throw RuntimeException("Unknown operator: $op")
}
}
private fun increaseTab(): String {
val old = tab
tab += " "
return old
}
companion object {
private val IGNORED_CHILDREN =
EnumSet.of(
NodeType.LINE_COMMENT,
NodeType.BLOCK_COMMENT,
NodeType.SHEBANG,
NodeType.SEMICOLON,
NodeType.TERMINAL,
NodeType.OPERATOR,
NodeType.STRING_NEWLINE,
)
private val UNPACK_CHILDREN =
EnumSet.of(
NodeType.MODULE_DEFINITION,
NodeType.IMPORT_LIST,
NodeType.IMPORT_ALIAS,
NodeType.TYPEALIAS_HEADER,
NodeType.TYPEALIAS_BODY,
NodeType.CLASS_PROPERTY_HEADER,
NodeType.CLASS_PROPERTY_HEADER_BEGIN,
NodeType.CLASS_PROPERTY_BODY,
NodeType.CLASS_METHOD_HEADER,
NodeType.CLASS_METHOD_BODY,
NodeType.CLASS_HEADER,
NodeType.CLASS_HEADER_EXTENDS,
NodeType.CLASS_BODY_ELEMENTS,
NodeType.MODIFIER_LIST,
NodeType.NEW_HEADER,
NodeType.OBJECT_MEMBER_LIST,
NodeType.OBJECT_ENTRY_HEADER,
NodeType.OBJECT_PROPERTY_HEADER,
NodeType.OBJECT_PROPERTY_HEADER_BEGIN,
NodeType.OBJECT_PROPERTY_BODY,
NodeType.OBJECT_PARAMETER_LIST,
NodeType.FOR_GENERATOR_HEADER,
NodeType.FOR_GENERATOR_HEADER_DEFINITION,
NodeType.FOR_GENERATOR_HEADER_DEFINITION_HEADER,
NodeType.WHEN_GENERATOR_HEADER,
NodeType.IF_HEADER,
NodeType.IF_CONDITION,
NodeType.IF_CONDITION_EXPR,
NodeType.IF_THEN_EXPR,
NodeType.IF_ELSE_EXPR,
NodeType.FUNCTION_LITERAL_BODY,
NodeType.ARGUMENT_LIST_ELEMENTS,
NodeType.PARAMETER_LIST_ELEMENTS,
NodeType.CONSTRAINED_TYPE_CONSTRAINT,
NodeType.CONSTRAINED_TYPE_ELEMENTS,
NodeType.TYPE_PARAMETER_LIST_ELEMENTS,
NodeType.TYPE_ARGUMENT_LIST_ELEMENTS,
NodeType.LET_PARAMETER_DEFINITION,
NodeType.LET_PARAMETER,
NodeType.PARENTHESIZED_EXPR_ELEMENTS,
NodeType.PARENTHESIZED_TYPE_ELEMENTS,
NodeType.FUNCTION_TYPE_PARAMETERS,
)
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright © 2024-2025 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.parser
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.extension
import kotlin.io.path.pathString
import kotlin.io.path.readText
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.parallel.Execution
import org.junit.jupiter.api.parallel.ExecutionMode
import org.pkl.commons.walk
@Execution(ExecutionMode.CONCURRENT)
class ParserComparisonTest {
@Test
fun compareSnippetTests() {
SoftAssertions.assertSoftly { softly ->
getSnippets()
.parallelStream()
.map { Pair(it.pathString, it.readText()) }
.forEach { (path, snippet) ->
try {
compare(snippet, path, softly)
} catch (e: GenericParserError) {
softly.fail("path: $path. Message: ${e.message}", e)
}
}
}
}
fun getSnippets(): List<Path> {
return Path("../pkl-core/src/test/files/LanguageSnippetTests/input")
.walk()
.filter { path ->
val pathStr = path.toString().replace("\\", "/")
path.extension == "pkl" &&
!exceptions.any { pathStr.endsWith(it) } &&
!regexExceptions.any { it.matches(pathStr) }
}
.toList()
}
fun compare(code: String, path: String? = null, softly: SoftAssertions? = null) {
val (sexp, genSexp) = renderBoth(code)
when {
(path != null && softly != null) ->
softly.assertThat(genSexp).`as`("path: $path").isEqualTo(sexp)
else -> assertThat(genSexp).`as`("path: $path").isEqualTo(sexp)
}
}
fun renderBoth(code: String): Pair<String, String> =
Pair(renderCode(code), renderGenericCode(code))
companion object {
private fun renderCode(code: String): String {
val parser = Parser()
val mod = parser.parseModule(code)
val renderer = SexpRenderer()
return renderer.render(mod)
}
private fun renderGenericCode(code: String): String {
val parser = GenericParser()
val mod = parser.parseModule(code)
val renderer = GenericSexpRenderer(code)
return renderer.render(mod)
}
// tests that are not syntactically valid Pkl
private val exceptions =
setOf(
"stringError1.pkl",
"annotationIsNotExpression2.pkl",
"amendsRequiresParens.pkl",
"errors/parser18.pkl",
"errors/nested1.pkl",
"errors/invalidCharacterEscape.pkl",
"errors/invalidUnicodeEscape.pkl",
"errors/unterminatedUnicodeEscape.pkl",
"errors/keywordNotAllowedHere1.pkl",
"errors/keywordNotAllowedHere2.pkl",
"errors/keywordNotAllowedHere3.pkl",
"errors/keywordNotAllowedHere4.pkl",
"errors/moduleWithHighMinPklVersionAndParseErrors.pkl",
"errors/underscore.pkl",
"errors/shebang.pkl",
"notAUnionDefault.pkl",
"multipleDefaults.pkl",
"modules/invalidModule1.pkl",
)
private val regexExceptions =
setOf(
Regex(".*/errors/delimiters/.*"),
Regex(".*/errors/parser\\d+\\.pkl"),
Regex(".*/parser/.*"),
)
}
}

View File

@@ -82,13 +82,22 @@ class SexpRenderer {
} }
if (decl.extendsOrAmendsDecl !== null) { if (decl.extendsOrAmendsDecl !== null) {
buf.append('\n') buf.append('\n')
buf.append(tab) renderExtendsOrAmendsClause(decl.extendsOrAmendsDecl!!)
buf.append("(extendsOrAmendsClause)")
} }
tab = oldTab tab = oldTab
buf.append(')') buf.append(')')
} }
fun renderExtendsOrAmendsClause(clause: ExtendsOrAmendsClause) {
buf.append(tab)
buf.append("(extendsOrAmendsClause")
val oldTab = increaseTab()
buf.append('\n')
renderStringConstant(clause.url)
tab = oldTab
buf.append(')')
}
fun renderImport(imp: ImportClause) { fun renderImport(imp: ImportClause) {
buf.append(tab) buf.append(tab)
if (imp.isGlob) { if (imp.isGlob) {
@@ -97,6 +106,8 @@ class SexpRenderer {
buf.append("(importClause") buf.append("(importClause")
} }
val oldTab = increaseTab() val oldTab = increaseTab()
buf.append('\n')
renderStringConstant(imp.importStr)
if (imp.alias !== null) { if (imp.alias !== null) {
buf.append('\n') buf.append('\n')
buf.append(tab) buf.append(tab)
@@ -178,6 +189,7 @@ class SexpRenderer {
buf.append("(identifier)") buf.append("(identifier)")
val tparList = `typealias`.typeParameterList val tparList = `typealias`.typeParameterList
if (tparList !== null) { if (tparList !== null) {
buf.append('\n')
renderTypeParameterList(tparList) renderTypeParameterList(tparList)
} }
buf.append('\n') buf.append('\n')
@@ -244,6 +256,7 @@ class SexpRenderer {
buf.append("(identifier)") buf.append("(identifier)")
val tparList = classMethod.typeParameterList val tparList = classMethod.typeParameterList
if (tparList !== null) { if (tparList !== null) {
buf.append('\n')
renderTypeParameterList(tparList) renderTypeParameterList(tparList)
} }
buf.append('\n') buf.append('\n')
@@ -385,11 +398,7 @@ class SexpRenderer {
is MultiLineStringLiteralExpr -> renderMultiLineStringLiteral(expr) is MultiLineStringLiteralExpr -> renderMultiLineStringLiteral(expr)
is ThrowExpr -> renderThrowExpr(expr) is ThrowExpr -> renderThrowExpr(expr)
is TraceExpr -> renderTraceExpr(expr) is TraceExpr -> renderTraceExpr(expr)
is ImportExpr -> { is ImportExpr -> renderImportExpr(expr)
buf.append(tab)
val name = if (expr.isGlob) "(importGlobExpr)" else "(importExpr)"
buf.append(name)
}
is ReadExpr -> renderReadExpr(expr) is ReadExpr -> renderReadExpr(expr)
is UnqualifiedAccessExpr -> renderUnqualifiedAccessExpr(expr) is UnqualifiedAccessExpr -> renderUnqualifiedAccessExpr(expr)
is QualifiedAccessExpr -> renderQualifiedAccessExpr(expr) is QualifiedAccessExpr -> renderQualifiedAccessExpr(expr)
@@ -399,7 +408,7 @@ class SexpRenderer {
is IfExpr -> renderIfExpr(expr) is IfExpr -> renderIfExpr(expr)
is LetExpr -> renderLetExpr(expr) is LetExpr -> renderLetExpr(expr)
is FunctionLiteralExpr -> renderFunctionLiteralExpr(expr) is FunctionLiteralExpr -> renderFunctionLiteralExpr(expr)
is ParenthesizedExpr -> renderParenthesisedExpr(expr) is ParenthesizedExpr -> renderParenthesizedExpr(expr)
is NewExpr -> renderNewExpr(expr) is NewExpr -> renderNewExpr(expr)
is AmendsExpr -> renderAmendsExpr(expr) is AmendsExpr -> renderAmendsExpr(expr)
is NonNullExpr -> renderNonNullExpr(expr) is NonNullExpr -> renderNonNullExpr(expr)
@@ -421,7 +430,7 @@ class SexpRenderer {
renderExpr(part.expr) renderExpr(part.expr)
} else { } else {
buf.append('\n').append(tab) buf.append('\n').append(tab)
buf.append("(stringConstantExpr)") buf.append("(stringConstant)")
} }
} }
buf.append(')') buf.append(')')
@@ -480,6 +489,17 @@ class SexpRenderer {
tab = oldTab tab = oldTab
} }
fun renderImportExpr(expr: ImportExpr) {
buf.append(tab)
val name = if (expr.isGlob) "(importGlobExpr" else "(importExpr"
buf.append(name)
val oldTab = increaseTab()
buf.append('\n')
renderStringConstant(expr.importStr)
buf.append(')')
tab = oldTab
}
fun renderUnqualifiedAccessExpr(expr: UnqualifiedAccessExpr) { fun renderUnqualifiedAccessExpr(expr: UnqualifiedAccessExpr) {
buf.append(tab) buf.append(tab)
buf.append("(unqualifiedAccessExpr") buf.append("(unqualifiedAccessExpr")
@@ -517,6 +537,7 @@ class SexpRenderer {
buf.append("(superAccessExpr") buf.append("(superAccessExpr")
val oldTab = increaseTab() val oldTab = increaseTab()
buf.append('\n') buf.append('\n')
buf.append(tab)
buf.append("(identifier)") buf.append("(identifier)")
if (expr.argumentList !== null) { if (expr.argumentList !== null) {
buf.append('\n') buf.append('\n')
@@ -588,7 +609,7 @@ class SexpRenderer {
tab = oldTab tab = oldTab
} }
fun renderParenthesisedExpr(expr: ParenthesizedExpr) { fun renderParenthesizedExpr(expr: ParenthesizedExpr) {
buf.append(tab) buf.append(tab)
buf.append("(parenthesizedExpr") buf.append("(parenthesizedExpr")
val oldTab = increaseTab() val oldTab = increaseTab()
@@ -713,6 +734,11 @@ class SexpRenderer {
tab = oldTab tab = oldTab
} }
fun renderStringConstant(str: StringConstant) {
buf.append(tab)
buf.append("(stringConstant)")
}
fun renderTypeAnnotation(typeAnnotation: TypeAnnotation) { fun renderTypeAnnotation(typeAnnotation: TypeAnnotation) {
buf.append(tab) buf.append(tab)
buf.append("(typeAnnotation") buf.append("(typeAnnotation")
@@ -737,10 +763,7 @@ class SexpRenderer {
buf.append(tab) buf.append(tab)
buf.append("(moduleType)") buf.append("(moduleType)")
} }
is StringConstantType -> { is StringConstantType -> renderStringConstantType(type)
buf.append(tab)
buf.append("(stringConstantType)")
}
is DeclaredType -> renderDeclaredType(type) is DeclaredType -> renderDeclaredType(type)
is ParenthesizedType -> renderParenthesizedType(type) is ParenthesizedType -> renderParenthesizedType(type)
is NullableType -> renderNullableType(type) is NullableType -> renderNullableType(type)
@@ -750,6 +773,16 @@ class SexpRenderer {
} }
} }
fun renderStringConstantType(type: StringConstantType) {
buf.append(tab)
buf.append("(stringConstantType")
val oldTab = increaseTab()
buf.append('\n')
renderStringConstant(type.str)
buf.append(')')
tab = oldTab
}
fun renderDeclaredType(type: DeclaredType) { fun renderDeclaredType(type: DeclaredType) {
buf.append(tab) buf.append(tab)
buf.append("(declaredType") buf.append("(declaredType")
@@ -778,7 +811,7 @@ class SexpRenderer {
fun renderParenthesizedType(type: ParenthesizedType) { fun renderParenthesizedType(type: ParenthesizedType) {
buf.append(tab) buf.append(tab)
buf.append("(parenthesisedType") buf.append("(parenthesizedType")
val oldTab = increaseTab() val oldTab = increaseTab()
buf.append('\n') buf.append('\n')
renderType(type.type) renderType(type.type)
@@ -903,12 +936,11 @@ class SexpRenderer {
buf.append(tab) buf.append(tab)
buf.append("(objectMethod") buf.append("(objectMethod")
val oldTab = increaseTab() val oldTab = increaseTab()
buf.append('\n')
for (mod in method.modifiers) { for (mod in method.modifiers) {
buf.append('\n') buf.append('\n')
renderModifier(mod) renderModifier(mod)
} }
buf.append('\n') buf.append('\n').append(tab)
buf.append("(identifier)") buf.append("(identifier)")
val tparList = method.typeParameterList val tparList = method.typeParameterList
if (tparList !== null) { if (tparList !== null) {
@@ -1012,19 +1044,20 @@ class SexpRenderer {
fun renderTypeParameterList(typeParameterList: TypeParameterList) { fun renderTypeParameterList(typeParameterList: TypeParameterList) {
buf.append(tab) buf.append(tab)
buf.append("(TypeParameterList\n") buf.append("(typeParameterList")
val oldTab = increaseTab() val oldTab = increaseTab()
for (tpar in typeParameterList.parameters) { for (tpar in typeParameterList.parameters) {
buf.append('\n') buf.append('\n')
renderTypeParameter(tpar) renderTypeParameter(tpar)
} }
buf.append(')')
tab = oldTab tab = oldTab
} }
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun renderTypeParameter(ignored: TypeParameter?) { fun renderTypeParameter(ignored: TypeParameter?) {
buf.append(tab) buf.append(tab)
buf.append("(TypeParameter\n") buf.append("(typeParameter\n")
val oldTab = increaseTab() val oldTab = increaseTab()
buf.append(tab) buf.append(tab)
buf.append("(identifier))") buf.append("(identifier))")

View File

@@ -41,6 +41,8 @@ include("pkl-doc")
include("pkl-executor") include("pkl-executor")
include("pkl-formatter")
include("pkl-gradle") include("pkl-gradle")
include("pkl-parser") include("pkl-parser")