mirror of
https://github.com/apple/pkl.git
synced 2026-05-24 15:56:56 +02:00
Move pkl-formatter to Java (#1514)
This commit is contained in:
@@ -24,22 +24,21 @@ org.jetbrains.kotlin:kotlin-daemon-client:2.2.21=kotlinBuildToolsApiClasspath
|
||||
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
|
||||
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:2.2.21=kotlinKlibCommonizerClasspath
|
||||
org.jetbrains.kotlin:kotlin-metadata-jvm:2.2.21=kotlinInternalAbiValidation
|
||||
org.jetbrains.kotlin:kotlin-native-prebuilt:2.0.21=kotlinNativeBundleConfiguration
|
||||
org.jetbrains.kotlin:kotlin-reflect:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
|
||||
org.jetbrains.kotlin:kotlin-script-runtime:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
|
||||
org.jetbrains.kotlin:kotlin-scripting-common:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
|
||||
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
|
||||
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
|
||||
org.jetbrains.kotlin:kotlin-scripting-jvm:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,runtimeClasspath,swiftExportClasspathResolvable,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.jetbrains.kotlin:swift-export-embeddable:2.2.21=swiftExportClasspathResolvable
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.3=swiftExportClasspathResolvable
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=swiftExportClasspathResolvable
|
||||
org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3=swiftExportClasspathResolvable
|
||||
org.jetbrains:annotations:13.0=compileClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,runtimeClasspath,swiftExportClasspathResolvable,testCompileClasspath,testRuntimeClasspath
|
||||
org.jetbrains:annotations:13.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinInternalAbiValidation,kotlinKlibCommonizerClasspath,swiftExportClasspathResolvable,testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=testCompileClasspath,testImplementationDependenciesMetadata
|
||||
org.junit.jupiter:junit-jupiter-api:6.0.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:6.0.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
@@ -50,4 +49,4 @@ org.junit.platform:junit-platform-launcher:6.0.3=testRuntimeClasspath
|
||||
org.junit:junit-bom:6.0.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
org.msgpack:msgpack-core:0.9.11=testRuntimeClasspath
|
||||
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
|
||||
empty=annotationProcessor,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,signatures,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDefExtensions
|
||||
empty=annotationProcessor,apiDependenciesMetadata,compileClasspath,compileOnlyDependenciesMetadata,implementationDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,runtimeClasspath,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDefExtensions
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2025-2026 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.
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
plugins {
|
||||
pklAllProjects
|
||||
pklKotlinLibrary
|
||||
pklJavaLibrary
|
||||
pklPublishLibrary
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright © 2026 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.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
sealed interface FormatNode
|
||||
permits Text,
|
||||
Empty,
|
||||
Line,
|
||||
ForceLine,
|
||||
SpaceOrLine,
|
||||
Space,
|
||||
Indent,
|
||||
Nodes,
|
||||
Group,
|
||||
MultilineStringGroup,
|
||||
IfWrap {
|
||||
|
||||
default int width(Set<Integer> wrapped) {
|
||||
if (this instanceof Nodes n) {
|
||||
return n.nodes().stream().mapToInt(node -> node.width(wrapped)).sum();
|
||||
} else if (this instanceof Group g) {
|
||||
return g.nodes().stream().mapToInt(node -> node.width(wrapped)).sum();
|
||||
} else if (this instanceof Indent i) {
|
||||
return i.nodes().stream().mapToInt(node -> node.width(wrapped)).sum();
|
||||
} else if (this instanceof IfWrap iw) {
|
||||
return wrapped.contains(iw.id()) ? iw.ifWrap().width(wrapped) : iw.ifNotWrap().width(wrapped);
|
||||
} else if (this instanceof Text t) {
|
||||
return t.text().length();
|
||||
} else if (this instanceof SpaceOrLine || this instanceof Space) {
|
||||
return 1;
|
||||
} else if (this instanceof ForceLine || this instanceof MultilineStringGroup) {
|
||||
return Generator.MAX;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record Text(String text) implements FormatNode {}
|
||||
|
||||
record Empty() implements FormatNode {
|
||||
static final Empty INSTANCE = new Empty();
|
||||
}
|
||||
|
||||
record Line() implements FormatNode {
|
||||
static final Line INSTANCE = new Line();
|
||||
}
|
||||
|
||||
record ForceLine() implements FormatNode {
|
||||
static final ForceLine INSTANCE = new ForceLine();
|
||||
}
|
||||
|
||||
record SpaceOrLine() implements FormatNode {
|
||||
static final SpaceOrLine INSTANCE = new SpaceOrLine();
|
||||
}
|
||||
|
||||
record Space() implements FormatNode {
|
||||
static final Space INSTANCE = new Space();
|
||||
}
|
||||
|
||||
record Indent(List<FormatNode> nodes) implements FormatNode {}
|
||||
|
||||
record Nodes(List<FormatNode> nodes) implements FormatNode {}
|
||||
|
||||
record Group(int id, List<FormatNode> nodes) implements FormatNode {}
|
||||
|
||||
record MultilineStringGroup(int endQuoteCol, List<FormatNode> nodes) implements FormatNode {}
|
||||
|
||||
record IfWrap(int id, FormatNode ifWrap, FormatNode ifNotWrap) implements FormatNode {}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright © 2026 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.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.pkl.parser.GenericParser;
|
||||
|
||||
/**
|
||||
* A formatter for Pkl files that applies canonical formatting rules.
|
||||
*
|
||||
* @see GrammarVersion
|
||||
*/
|
||||
public class Formatter {
|
||||
|
||||
private final GrammarVersion grammarVersion;
|
||||
|
||||
public Formatter(GrammarVersion grammarVersion) {
|
||||
this.grammarVersion = grammarVersion;
|
||||
}
|
||||
|
||||
public Formatter() {
|
||||
this(GrammarVersion.latest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @deprecated use {@code format(Files.readString(path))} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public String format(Path path, GrammarVersion grammarVersion) throws IOException {
|
||||
return new Formatter(grammarVersion).format(Files.readString(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @deprecated use {@code format(Files.readString(path))} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public String format(Path path) throws IOException {
|
||||
return format(path, GrammarVersion.latest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @deprecated use {@code new Formatter(grammarVersion).format(text)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public String format(String text, GrammarVersion grammarVersion) {
|
||||
return new Formatter(grammarVersion).format(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given Pkl source code text.
|
||||
*
|
||||
* @param text the Pkl source code to format
|
||||
* @return the formatted Pkl source code as a string
|
||||
*/
|
||||
public String format(String text) {
|
||||
var sb = new StringBuilder();
|
||||
format(text, sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given Pkl source code text.
|
||||
*
|
||||
* <p>It is the caller's responsibility to close {@code input}, and, if applicable, {@code
|
||||
* output}.
|
||||
*
|
||||
* @param input the Pkl source code to format
|
||||
* @param output the formatted Pkl source code
|
||||
* @throws IOException if an I/O error occurs during reading or writing
|
||||
*/
|
||||
public void format(Reader input, Appendable output) throws IOException {
|
||||
var sb = new StringBuilder();
|
||||
var buf = new char[8192];
|
||||
int n;
|
||||
while ((n = input.read(buf)) != -1) {
|
||||
sb.append(buf, 0, n);
|
||||
}
|
||||
format(sb.toString(), output);
|
||||
}
|
||||
|
||||
private void format(String input, Appendable output) {
|
||||
var ast = new GenericParser().parseModule(input);
|
||||
var formatAst = new Builder(input, grammarVersion).format(ast);
|
||||
// force a line at the end of the file
|
||||
var nodes = new Nodes(List.of(formatAst, ForceLine.INSTANCE));
|
||||
new Generator(output).generate(nodes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright © 2026 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.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
final class Generator {
|
||||
|
||||
static final int MAX = 100;
|
||||
private static final String INDENT = " ";
|
||||
|
||||
private final Appendable buf;
|
||||
private int indent = 0;
|
||||
private int size = 0;
|
||||
private final Set<Integer> wrapped = new HashSet<>();
|
||||
private boolean shouldAddIndent = false;
|
||||
|
||||
Generator(Appendable buf) {
|
||||
this.buf = buf;
|
||||
}
|
||||
|
||||
void generate(FormatNode node) {
|
||||
node(node, Wrap.DETECT);
|
||||
}
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private void node(FormatNode node, Wrap wrap) {
|
||||
if (node instanceof Empty) {
|
||||
// nothing
|
||||
} else if (node instanceof Nodes n) {
|
||||
for (var child : n.nodes()) {
|
||||
node(child, wrap);
|
||||
}
|
||||
} else if (node instanceof Group g) {
|
||||
var width = 0;
|
||||
for (var child : g.nodes()) {
|
||||
width += child.width(wrapped);
|
||||
}
|
||||
var groupWrap = wrap;
|
||||
if (size + width > MAX) {
|
||||
wrapped.add(g.id());
|
||||
groupWrap = Wrap.ENABLED;
|
||||
} else {
|
||||
groupWrap = Wrap.DETECT;
|
||||
}
|
||||
for (var child : g.nodes()) {
|
||||
node(child, groupWrap);
|
||||
}
|
||||
} else if (node instanceof IfWrap iw) {
|
||||
if (wrapped.contains(iw.id())) {
|
||||
node(iw.ifWrap(), Wrap.ENABLED);
|
||||
} else {
|
||||
node(iw.ifNotWrap(), wrap);
|
||||
}
|
||||
} else if (node instanceof Text t) {
|
||||
text(t.text());
|
||||
} else if (node instanceof Line) {
|
||||
if (wrap.isEnabled()) {
|
||||
newline(true);
|
||||
}
|
||||
} else if (node instanceof ForceLine) {
|
||||
newline(true);
|
||||
} else if (node instanceof SpaceOrLine) {
|
||||
if (wrap.isEnabled()) {
|
||||
newline(true);
|
||||
} else {
|
||||
text(" ");
|
||||
}
|
||||
} else if (node instanceof Space) {
|
||||
text(" ");
|
||||
} else if (node instanceof Indent ind) {
|
||||
if (wrap.isEnabled() && !ind.nodes().isEmpty()) {
|
||||
size += INDENT.length();
|
||||
indent++;
|
||||
for (var child : ind.nodes()) {
|
||||
node(child, wrap);
|
||||
}
|
||||
indent--;
|
||||
} else {
|
||||
for (var child : ind.nodes()) {
|
||||
node(child, wrap);
|
||||
}
|
||||
}
|
||||
} else if (node instanceof MultilineStringGroup multi) {
|
||||
var indentLength = indent * INDENT.length();
|
||||
var oldIndent = indentFor(multi);
|
||||
var previousNewline = false;
|
||||
var nodes = multi.nodes();
|
||||
for (var i = 0; i < nodes.size(); i++) {
|
||||
var child = nodes.get(i);
|
||||
if (child instanceof ForceLine) {
|
||||
newline(false);
|
||||
} else if (child instanceof Text t
|
||||
&& previousNewline
|
||||
&& t.text().isBlank()
|
||||
&& t.text().length() == oldIndent
|
||||
&& nodes.get(i + 1) instanceof ForceLine) {
|
||||
// skip blank line indentation that will be repositioned
|
||||
} else if (child instanceof Text t && previousNewline) {
|
||||
text(reposition(t.text(), multi.endQuoteCol() - 1, indentLength));
|
||||
} else {
|
||||
node(child, Wrap.DETECT);
|
||||
}
|
||||
previousNewline = child instanceof ForceLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void text(String value) {
|
||||
try {
|
||||
if (shouldAddIndent) {
|
||||
for (var i = 0; i < indent; i++) {
|
||||
buf.append(INDENT);
|
||||
}
|
||||
shouldAddIndent = false;
|
||||
}
|
||||
size += value.length();
|
||||
buf.append(value);
|
||||
} catch (java.io.IOException e) {
|
||||
throw new java.io.UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void newline(boolean shouldIndent) {
|
||||
try {
|
||||
size = INDENT.length() * indent;
|
||||
buf.append('\n');
|
||||
shouldAddIndent = shouldIndent;
|
||||
} catch (java.io.IOException e) {
|
||||
throw new java.io.UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// accept text indented by originalOffset characters (tabs or spaces)
|
||||
// and return it indented by newOffset characters (spaces only)
|
||||
private static String reposition(String text, int originalOffset, int newOffset) {
|
||||
return " ".repeat(newOffset) + text.substring(originalOffset);
|
||||
}
|
||||
|
||||
private static int indentFor(MultilineStringGroup multi) {
|
||||
var nodes = multi.nodes();
|
||||
if (nodes.size() < 2) return 0;
|
||||
var beforeLast = nodes.get(nodes.size() - 2);
|
||||
return beforeLast instanceof Text t ? t.text().length() : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright © 2026 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;
|
||||
|
||||
/** Grammar compatibility version. */
|
||||
public enum GrammarVersion {
|
||||
V1(1, "0.25 - 0.29"),
|
||||
V2(2, "0.30+");
|
||||
|
||||
private final int version;
|
||||
private final String versionSpan;
|
||||
|
||||
GrammarVersion(int version, String versionSpan) {
|
||||
this.version = version;
|
||||
this.versionSpan = versionSpan;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getVersionSpan() {
|
||||
return versionSpan;
|
||||
}
|
||||
|
||||
public static GrammarVersion latest() {
|
||||
var latest = V1;
|
||||
for (var v : values()) {
|
||||
if (v.version > latest.version) {
|
||||
latest = v;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright © 2026 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.util.Comparator;
|
||||
|
||||
final class NaturalOrderComparator implements Comparator<String> {
|
||||
|
||||
private final boolean ignoreCase;
|
||||
|
||||
NaturalOrderComparator(boolean ignoreCase) {
|
||||
this.ignoreCase = ignoreCase;
|
||||
}
|
||||
|
||||
NaturalOrderComparator() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(String s1, String s2) {
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
|
||||
while (i < s1.length() && j < s2.length()) {
|
||||
var c1 = ignoreCase ? Character.toLowerCase(s1.charAt(i)) : s1.charAt(i);
|
||||
var c2 = ignoreCase ? Character.toLowerCase(s2.charAt(j)) : s2.charAt(j);
|
||||
|
||||
if (Character.isDigit(c1) && Character.isDigit(c2)) {
|
||||
var pair1 = getNumber(s1, i);
|
||||
var pair2 = getNumber(s2, j);
|
||||
|
||||
var numComparison = Long.compare(pair1.l, pair2.l);
|
||||
if (numComparison != 0) {
|
||||
return numComparison;
|
||||
}
|
||||
i = pair1.i;
|
||||
j = pair2.i;
|
||||
} else {
|
||||
var charComparison = Character.compare(c1, c2);
|
||||
if (charComparison != 0) {
|
||||
return charComparison;
|
||||
}
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
return Integer.compare(s1.length(), s2.length());
|
||||
}
|
||||
|
||||
private static LongAndInt getNumber(String s, int startIndex) {
|
||||
var i = startIndex;
|
||||
while (i < s.length() && Character.isDigit(s.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
try {
|
||||
var number = Long.parseLong(s, startIndex, i, 10);
|
||||
return new LongAndInt(number, i);
|
||||
} catch (NumberFormatException e) {
|
||||
return new LongAndInt(0L, i);
|
||||
}
|
||||
}
|
||||
|
||||
private record LongAndInt(long l, int i) {}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright © 2026 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;
|
||||
|
||||
enum Wrap {
|
||||
ENABLED,
|
||||
DETECT;
|
||||
|
||||
boolean isEnabled() {
|
||||
return this == ENABLED;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 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.io.IOException
|
||||
import java.io.Reader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.jvm.Throws
|
||||
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.
|
||||
*
|
||||
* @param grammarVersion grammar compatibility version
|
||||
*/
|
||||
class Formatter
|
||||
@JvmOverloads
|
||||
constructor(private val grammarVersion: GrammarVersion = GrammarVersion.latest()) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Deprecated(message = "use format(path.readText()) instead")
|
||||
fun format(path: Path, grammarVersion: GrammarVersion = GrammarVersion.latest()): String {
|
||||
return Formatter(grammarVersion).format(Files.readString(path))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Deprecated(message = "use Formatter(grammarVersion).format(text) instead")
|
||||
fun format(text: String, grammarVersion: GrammarVersion): String {
|
||||
return Formatter(grammarVersion).format(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return buildString { format(text, this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given Pkl source code text.
|
||||
*
|
||||
* It is the caller's responsibility to close [input], and, if applicable, [output].
|
||||
*
|
||||
* @param input the Pkl source code to format
|
||||
* @param output the formatted Pkl source code
|
||||
* @throws java.io.IOException if an I/O error occurs during reading or writing
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun format(input: Reader, output: Appendable) {
|
||||
format(input.readText(), output)
|
||||
}
|
||||
|
||||
private fun format(input: String, output: Appendable) {
|
||||
val ast = GenericParser().parseModule(input)
|
||||
val formatAst = Builder(input, grammarVersion).format(ast)
|
||||
// force a line at the end of the file
|
||||
val nodes = Nodes(listOf(formatAst, ForceLine))
|
||||
Generator(output).generate(nodes)
|
||||
}
|
||||
}
|
||||
|
||||
/** Grammar compatibility version. */
|
||||
enum class GrammarVersion(val version: Int, val versionSpan: String) {
|
||||
V1(1, "0.25 - 0.29"),
|
||||
V2(2, "0.30+");
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun latest(): GrammarVersion = entries.maxBy { it.version }
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 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.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: Appendable) {
|
||||
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 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 oldIndent = indentFor(node)
|
||||
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 == oldIndent &&
|
||||
node.nodes[i + 1] is ForceLine -> {}
|
||||
child is Text && previousNewline ->
|
||||
text(reposition(child.text, node.endQuoteCol - 1, indentLength))
|
||||
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
|
||||
}
|
||||
|
||||
// accept text indented by originalOffset characters (tabs or spaces)
|
||||
// and return it indented by newOffset characters (spaces only)
|
||||
private fun reposition(text: String, originalOffset: Int, newOffset: Int): String =
|
||||
" ".repeat(newOffset) + text.drop(originalOffset)
|
||||
|
||||
// Returns the indent of this multiline string, which is the size of the last node before the
|
||||
// closing quotes, or 0 if the closing quotes have no indentation
|
||||
private fun indentFor(multi: MultilineStringGroup): Int {
|
||||
val nodes = multi.nodes
|
||||
if (nodes.size < 2) return 0
|
||||
val beforeLast = nodes[nodes.lastIndex - 1]
|
||||
return if (beforeLast is Text) beforeLast.text.length else 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
// max line length
|
||||
const val MAX = 100
|
||||
private const val INDENT = " "
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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 IfWrap -> if (id in wrapped) ifWrap.width(wrapped) else ifNotWrap.width(wrapped)
|
||||
is Text -> text.length
|
||||
SpaceOrLine,
|
||||
Space -> 1
|
||||
ForceLine,
|
||||
is MultilineStringGroup -> Generator.MAX
|
||||
Empty -> 0
|
||||
Line -> 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 MultilineStringGroup(val endQuoteCol: Int, val nodes: List<FormatNode>) : FormatNode
|
||||
|
||||
data class IfWrap(val id: Int, val ifWrap: FormatNode, val ifNotWrap: FormatNode) : FormatNode
|
||||
Reference in New Issue
Block a user