Add grammar compatibility option to the formatter (#1249)

This commit is contained in:
Islon Scherer
2025-10-28 13:29:08 +01:00
committed by GitHub
parent ef4989aa35
commit be0142d46b
9 changed files with 108 additions and 16 deletions

View File

@@ -743,6 +743,16 @@ 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`. If the path is a directory, recursively looks for files with a `.pkl` extension, or files named `PklProject`.
==== Options
.--grammar-version
[%collapsible]
====
Default: `2` (latest version) +
Select the grammar compatibility version for the formatter.
New versions are created for each backward incompatible grammar change.
====
[[command-format-apply]] [[command-format-apply]]
=== `pkl format apply` === `pkl format apply`
@@ -760,6 +770,14 @@ If the path is a directory, recursively looks for files with a `.pkl` extension,
Do not write the name of wrongly formatted files to stdout. Do not write the name of wrongly formatted files to stdout.
==== ====
.--grammar-version
[%collapsible]
====
Default: `2` (latest version) +
Select the grammar compatibility version for the formatter.
New versions are created for each backward incompatible grammar change.
====
[[common-options]] [[common-options]]
=== Common options === Common options

View File

@@ -21,12 +21,14 @@ import java.nio.file.Path
import kotlin.io.path.writeText import kotlin.io.path.writeText
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
import org.pkl.formatter.GrammarVersion
class CliFormatterApply( class CliFormatterApply(
cliBaseOptions: CliBaseOptions, cliBaseOptions: CliBaseOptions,
paths: List<Path>, paths: List<Path>,
grammarVersion: GrammarVersion,
private val silent: Boolean, private val silent: Boolean,
) : CliFormatterCommand(cliBaseOptions, paths) { ) : CliFormatterCommand(cliBaseOptions, paths, grammarVersion) {
override fun doRun() { override fun doRun() {
var status = 0 var status = 0

View File

@@ -19,9 +19,13 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
import org.pkl.formatter.GrammarVersion
class CliFormatterCheck(cliBaseOptions: CliBaseOptions, paths: List<Path>) : class CliFormatterCheck(
CliFormatterCommand(cliBaseOptions, paths) { cliBaseOptions: CliBaseOptions,
paths: List<Path>,
grammarVersion: GrammarVersion,
) : CliFormatterCommand(cliBaseOptions, paths, grammarVersion) {
override fun doRun() { override fun doRun() {
var status = 0 var status = 0

View File

@@ -25,6 +25,7 @@ import kotlin.io.path.walk
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliCommand import org.pkl.commons.cli.CliCommand
import org.pkl.formatter.Formatter import org.pkl.formatter.Formatter
import org.pkl.formatter.GrammarVersion
import org.pkl.parser.GenericParserError import org.pkl.parser.GenericParserError
abstract class CliFormatterCommand abstract class CliFormatterCommand
@@ -32,11 +33,12 @@ abstract class CliFormatterCommand
constructor( constructor(
options: CliBaseOptions, options: CliBaseOptions,
protected val paths: List<Path>, protected val paths: List<Path>,
protected val grammarVersion: GrammarVersion,
protected val consoleWriter: Writer = System.out.writer(), protected val consoleWriter: Writer = System.out.writer(),
) : CliCommand(options) { ) : CliCommand(options) {
protected fun format(file: Path, contents: String): Pair<String, Int> { protected fun format(file: Path, contents: String): Pair<String, Int> {
try { try {
return Formatter().format(contents) to 0 return Formatter().format(contents, grammarVersion) to 0
} catch (pe: GenericParserError) { } catch (pe: GenericParserError) {
consoleWriter.write("Could not format `$file`: $pe") consoleWriter.write("Could not format `$file`: $pe")
consoleWriter.appendLine() consoleWriter.appendLine()

View File

@@ -20,13 +20,17 @@ import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path import java.nio.file.Path
import org.pkl.cli.CliFormatterApply import org.pkl.cli.CliFormatterApply
import org.pkl.cli.CliFormatterCheck import org.pkl.cli.CliFormatterCheck
import org.pkl.cli.commands.FormatterCheckCommand.Companion.grammarVersionHelp
import org.pkl.commons.cli.commands.BaseCommand import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.formatter.GrammarVersion
class FormatterCommand : NoOpCliktCommand(name = "format") { class FormatterCommand : NoOpCliktCommand(name = "format") {
override fun help(context: Context) = "Run commands related to formatting" override fun help(context: Context) = "Run commands related to formatting"
@@ -47,8 +51,25 @@ class FormatterCheckCommand : BaseCommand(name = "check", helpLink = helpLink) {
.path(mustExist = true, canBeDir = true) .path(mustExist = true, canBeDir = true)
.multiple() .multiple()
val grammarVersion: GrammarVersion by
option(names = arrayOf("--grammar-version"), help = grammarVersionHelp)
.enum<GrammarVersion> { "${it.version}" }
.default(GrammarVersion.latest())
override fun run() { override fun run() {
CliFormatterCheck(baseOptions.baseOptions(emptyList()), paths).run() CliFormatterCheck(baseOptions.baseOptions(emptyList()), paths, grammarVersion).run()
}
companion object {
internal val grammarVersionHelp =
"""
The grammar compatibility version to use.$NEWLINE
${GrammarVersion.entries.joinToString("$NEWLINE", prefix = " ") {
val default = if (it == GrammarVersion.latest()) " `(default)`" else ""
"`${it.version}`: ${it.versionSpan}$default"
}}
"""
.trimIndent()
} }
} }
@@ -68,7 +89,12 @@ class FormatterApplyCommand : BaseCommand(name = "apply", helpLink = helpLink) {
) )
.flag() .flag()
val grammarVersion: GrammarVersion by
option(names = arrayOf("--grammar-version"), help = grammarVersionHelp)
.enum<GrammarVersion> { "${it.version}" }
.default(GrammarVersion.latest())
override fun run() { override fun run() {
CliFormatterApply(baseOptions.baseOptions(emptyList()), paths, silent).run() CliFormatterApply(baseOptions.baseOptions(emptyList()), paths, grammarVersion, silent).run()
} }
} }

View File

@@ -33,8 +33,6 @@ import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.TestOptions import org.pkl.commons.cli.commands.TestOptions
import org.pkl.commons.cli.commands.single import org.pkl.commons.cli.commands.single
private const val NEWLINE = '\u0085'
class ProjectCommand : NoOpCliktCommand(name = "project") { class ProjectCommand : NoOpCliktCommand(name = "project") {
override fun help(context: Context) = "Run commands related to projects" override fun help(context: Context) = "Run commands related to projects"

View File

@@ -0,0 +1,18 @@
/*
* 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
internal const val NEWLINE = '\u0085'

View File

@@ -33,7 +33,7 @@ import org.pkl.parser.syntax.Operator
import org.pkl.parser.syntax.generic.Node import org.pkl.parser.syntax.generic.Node
import org.pkl.parser.syntax.generic.NodeType import org.pkl.parser.syntax.generic.NodeType
internal class Builder(sourceText: String) { internal class Builder(sourceText: String, private val grammarVersion: GrammarVersion) {
private var id: Int = 0 private var id: Int = 0
private val source: CharArray = sourceText.toCharArray() private val source: CharArray = sourceText.toCharArray()
private var prevNode: Node? = null private var prevNode: Node? = null
@@ -542,7 +542,11 @@ internal class Builder(sourceText: String) {
if (prev.isTerminal("(") || next.isTerminal(")")) { if (prev.isTerminal("(") || next.isTerminal(")")) {
if (next.isTerminal(")")) { if (next.isTerminal(")")) {
// trailing comma // trailing comma
ifWrap(groupId, nodes(Text(","), line()), line()) if (grammarVersion == GrammarVersion.V1) {
line()
} else {
ifWrap(groupId, nodes(Text(","), line()), line())
}
} else line() } else line()
} else spaceOrLine() } else spaceOrLine()
} }
@@ -561,7 +565,11 @@ internal class Builder(sourceText: String) {
val node = if (hasTrailingLambda) Empty else line() val node = if (hasTrailingLambda) Empty else line()
if (next.isTerminal(")") && !hasTrailingLambda) { if (next.isTerminal(")") && !hasTrailingLambda) {
// trailing comma // trailing comma
ifWrap(groupId, nodes(Text(","), node), node) if (grammarVersion == GrammarVersion.V1) {
node
} else {
ifWrap(groupId, nodes(Text(","), node), node)
}
} else node } else node
} else spaceOrLine() } else spaceOrLine()
}, },
@@ -664,7 +672,11 @@ internal class Builder(sourceText: String) {
if (prev.isTerminal("<") || next.isTerminal(">")) { if (prev.isTerminal("<") || next.isTerminal(">")) {
if (next.isTerminal(">")) { if (next.isTerminal(">")) {
// trailing comma // trailing comma
ifWrap(id, nodes(Text(","), line()), line()) if (grammarVersion == GrammarVersion.V1) {
Line
} else {
ifWrap(id, nodes(Text(","), line()), line())
}
} else line() } else line()
} else spaceOrLine() } else spaceOrLine()
} }

View File

@@ -27,22 +27,24 @@ class Formatter {
* Formats a Pkl file from the given file path. * Formats a Pkl file from the given file path.
* *
* @param path the path to the Pkl file to format * @param path the path to the Pkl file to format
* @param grammarVersion grammar compatibility version
* @return the formatted Pkl source code as a string * @return the formatted Pkl source code as a string
* @throws java.io.IOException if the file cannot be read * @throws java.io.IOException if the file cannot be read
*/ */
fun format(path: Path): String { fun format(path: Path, grammarVersion: GrammarVersion = GrammarVersion.latest()): String {
return format(Files.readString(path)) return format(Files.readString(path), grammarVersion)
} }
/** /**
* Formats the given Pkl source code text. * Formats the given Pkl source code text.
* *
* @param text the Pkl source code to format * @param text the Pkl source code to format
* @param grammarVersion grammar compatibility version
* @return the formatted Pkl source code as a string * @return the formatted Pkl source code as a string
*/ */
fun format(text: String): String { fun format(text: String, grammarVersion: GrammarVersion = GrammarVersion.latest()): String {
val parser = GenericParser() val parser = GenericParser()
val builder = Builder(text) val builder = Builder(text, grammarVersion)
val gen = Generator() val gen = Generator()
val ast = parser.parseModule(text) val ast = parser.parseModule(text)
val formatAst = builder.format(ast) val formatAst = builder.format(ast)
@@ -51,3 +53,13 @@ class Formatter {
return gen.toString() return gen.toString()
} }
} }
/** Grammar compatibility version. */
enum class GrammarVersion(val version: Int, val versionSpan: String) {
V1(1, "0.25 - 0.29"),
V2(2, "0.30+");
companion object {
fun latest(): GrammarVersion = entries.maxBy { it.version }
}
}