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

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