diff --git a/docs/modules/pkl-cli/pages/index.adoc b/docs/modules/pkl-cli/pages/index.adoc index 8f817341..1a87f3b0 100644 --- a/docs/modules/pkl-cli/pages/index.adoc +++ b/docs/modules/pkl-cli/pages/index.adoc @@ -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`. +==== 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]] === `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. ==== +.--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 diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterApply.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterApply.kt index 1a8beb1d..c52658b3 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterApply.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterApply.kt @@ -21,12 +21,14 @@ import java.nio.file.Path import kotlin.io.path.writeText import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliException +import org.pkl.formatter.GrammarVersion class CliFormatterApply( cliBaseOptions: CliBaseOptions, paths: List, + grammarVersion: GrammarVersion, private val silent: Boolean, -) : CliFormatterCommand(cliBaseOptions, paths) { +) : CliFormatterCommand(cliBaseOptions, paths, grammarVersion) { override fun doRun() { var status = 0 diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCheck.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCheck.kt index f776c0af..419c18ba 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCheck.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCheck.kt @@ -19,9 +19,13 @@ import java.nio.file.Files import java.nio.file.Path import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliException +import org.pkl.formatter.GrammarVersion -class CliFormatterCheck(cliBaseOptions: CliBaseOptions, paths: List) : - CliFormatterCommand(cliBaseOptions, paths) { +class CliFormatterCheck( + cliBaseOptions: CliBaseOptions, + paths: List, + grammarVersion: GrammarVersion, +) : CliFormatterCommand(cliBaseOptions, paths, grammarVersion) { override fun doRun() { var status = 0 diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt index 63a2dbd3..0d466a14 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliFormatterCommand.kt @@ -25,6 +25,7 @@ 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.formatter.GrammarVersion import org.pkl.parser.GenericParserError abstract class CliFormatterCommand @@ -32,11 +33,12 @@ abstract class CliFormatterCommand constructor( options: CliBaseOptions, protected val paths: List, + protected val grammarVersion: GrammarVersion, protected val consoleWriter: Writer = System.out.writer(), ) : CliCommand(options) { protected fun format(file: Path, contents: String): Pair { try { - return Formatter().format(contents) to 0 + return Formatter().format(contents, grammarVersion) to 0 } catch (pe: GenericParserError) { consoleWriter.write("Could not format `$file`: $pe") consoleWriter.appendLine() diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/FormatterCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/FormatterCommand.kt index 7b8def78..bb5df7a0 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/FormatterCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/FormatterCommand.kt @@ -20,13 +20,17 @@ 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.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.option +import com.github.ajalt.clikt.parameters.types.enum 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.cli.commands.FormatterCheckCommand.Companion.grammarVersionHelp import org.pkl.commons.cli.commands.BaseCommand +import org.pkl.formatter.GrammarVersion class FormatterCommand : NoOpCliktCommand(name = "format") { 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) .multiple() + val grammarVersion: GrammarVersion by + option(names = arrayOf("--grammar-version"), help = grammarVersionHelp) + .enum { "${it.version}" } + .default(GrammarVersion.latest()) + 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() + val grammarVersion: GrammarVersion by + option(names = arrayOf("--grammar-version"), help = grammarVersionHelp) + .enum { "${it.version}" } + .default(GrammarVersion.latest()) + override fun run() { - CliFormatterApply(baseOptions.baseOptions(emptyList()), paths, silent).run() + CliFormatterApply(baseOptions.baseOptions(emptyList()), paths, grammarVersion, silent).run() } } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt index 2fde8ee1..b78922d4 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/ProjectCommand.kt @@ -33,8 +33,6 @@ import org.pkl.commons.cli.commands.BaseCommand import org.pkl.commons.cli.commands.TestOptions import org.pkl.commons.cli.commands.single -private const val NEWLINE = '\u0085' - class ProjectCommand : NoOpCliktCommand(name = "project") { override fun help(context: Context) = "Run commands related to projects" diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/Utils.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/Utils.kt new file mode 100644 index 00000000..0d391609 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/Utils.kt @@ -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' diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt index 1e1c0933..6cd48e1c 100644 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt +++ b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt @@ -33,7 +33,7 @@ import org.pkl.parser.syntax.Operator import org.pkl.parser.syntax.generic.Node 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 val source: CharArray = sourceText.toCharArray() private var prevNode: Node? = null @@ -542,7 +542,11 @@ internal class Builder(sourceText: String) { if (prev.isTerminal("(") || next.isTerminal(")")) { if (next.isTerminal(")")) { // trailing comma - ifWrap(groupId, nodes(Text(","), line()), line()) + if (grammarVersion == GrammarVersion.V1) { + line() + } else { + ifWrap(groupId, nodes(Text(","), line()), line()) + } } else line() } else spaceOrLine() } @@ -561,7 +565,11 @@ internal class Builder(sourceText: String) { val node = if (hasTrailingLambda) Empty else line() if (next.isTerminal(")") && !hasTrailingLambda) { // trailing comma - ifWrap(groupId, nodes(Text(","), node), node) + if (grammarVersion == GrammarVersion.V1) { + node + } else { + ifWrap(groupId, nodes(Text(","), node), node) + } } else node } else spaceOrLine() }, @@ -664,7 +672,11 @@ internal class Builder(sourceText: String) { if (prev.isTerminal("<") || next.isTerminal(">")) { if (next.isTerminal(">")) { // trailing comma - ifWrap(id, nodes(Text(","), line()), line()) + if (grammarVersion == GrammarVersion.V1) { + Line + } else { + ifWrap(id, nodes(Text(","), line()), line()) + } } else line() } else spaceOrLine() } diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt index 47dd9982..44159c15 100644 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt +++ b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt @@ -27,22 +27,24 @@ class Formatter { * Formats a Pkl file from the given file path. * * @param path the path to the Pkl file to format + * @param grammarVersion grammar compatibility version * @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)) + fun format(path: Path, grammarVersion: GrammarVersion = GrammarVersion.latest()): String { + return format(Files.readString(path), grammarVersion) } /** * Formats the given Pkl source code text. * * @param text the Pkl source code to format + * @param grammarVersion grammar compatibility version * @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 builder = Builder(text) + val builder = Builder(text, grammarVersion) val gen = Generator() val ast = parser.parseModule(text) val formatAst = builder.format(ast) @@ -51,3 +53,13 @@ class Formatter { 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 } + } +}