From 1d74e2a869b6b0b937c9e4bed94c5297f432999f Mon Sep 17 00:00:00 2001 From: Islon Scherer Date: Tue, 14 Apr 2026 16:29:42 +0200 Subject: [PATCH] Move pkl-formatter to Java (#1514) --- pkl-formatter/gradle.lockfile | 11 +- pkl-formatter/pkl-formatter.gradle.kts | 4 +- .../main/java/org/pkl/formatter/Builder.java | 1779 +++++++++++++++++ .../java/org/pkl/formatter/FormatNode.java | 85 + .../java/org/pkl/formatter/Formatter.java | 121 ++ .../java/org/pkl/formatter/Generator.java | 160 ++ .../org/pkl/formatter/GrammarVersion.java | 48 + .../pkl/formatter/NaturalOrderComparator.java | 78 + .../src/main/java/org/pkl/formatter/Wrap.java | 25 + .../main/kotlin/org/pkl/formatter/Builder.kt | 1649 --------------- .../kotlin/org/pkl/formatter/Formatter.kt | 103 - .../kotlin/org/pkl/formatter/Generator.kt | 145 -- .../pkl/formatter/NaturalOrderComparator.kt | 66 - .../org/pkl/formatter/ast/FormatNode.kt | 64 - 14 files changed, 2303 insertions(+), 2035 deletions(-) create mode 100644 pkl-formatter/src/main/java/org/pkl/formatter/Builder.java create mode 100644 pkl-formatter/src/main/java/org/pkl/formatter/FormatNode.java create mode 100644 pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java create mode 100644 pkl-formatter/src/main/java/org/pkl/formatter/Generator.java create mode 100644 pkl-formatter/src/main/java/org/pkl/formatter/GrammarVersion.java create mode 100644 pkl-formatter/src/main/java/org/pkl/formatter/NaturalOrderComparator.java create mode 100644 pkl-formatter/src/main/java/org/pkl/formatter/Wrap.java delete mode 100644 pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt delete mode 100644 pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt delete mode 100644 pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt delete mode 100644 pkl-formatter/src/main/kotlin/org/pkl/formatter/NaturalOrderComparator.kt delete mode 100644 pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt diff --git a/pkl-formatter/gradle.lockfile b/pkl-formatter/gradle.lockfile index 207d8662..3110c809 100644 --- a/pkl-formatter/gradle.lockfile +++ b/pkl-formatter/gradle.lockfile @@ -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 diff --git a/pkl-formatter/pkl-formatter.gradle.kts b/pkl-formatter/pkl-formatter.gradle.kts index 995f58ec..f5ea674a 100644 --- a/pkl-formatter/pkl-formatter.gradle.kts +++ b/pkl-formatter/pkl-formatter.gradle.kts @@ -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 } diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java b/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java new file mode 100644 index 00000000..c33b88c8 --- /dev/null +++ b/pkl-formatter/src/main/java/org/pkl/formatter/Builder.java @@ -0,0 +1,1779 @@ +/* + * 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.ArrayList; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import org.pkl.parser.syntax.Operator; +import org.pkl.parser.syntax.generic.Node; +import org.pkl.parser.syntax.generic.NodeType; + +final class Builder { + + private int id = 0; + private final char[] source; + private boolean noNewlines = false; + private final GrammarVersion grammarVersion; + + Builder(String sourceText, GrammarVersion grammarVersion) { + this.source = sourceText.toCharArray(); + this.grammarVersion = grammarVersion; + } + + FormatNode format(Node node) { + return switch (node.type) { + case MODULE -> formatModule(node); + case DOC_COMMENT, + OBJECT_SPREAD, + NON_NULL_EXPR, + SUPER_ACCESS_EXPR, + IMPORT_EXPR, + UNARY_MINUS_EXPR, + LOGICAL_NOT_EXPR, + NULLABLE_TYPE -> + new Nodes(formatGeneric(node.children, (FormatNode) null)); + case DOC_COMMENT_LINE -> formatDocComment(node); + case LINE_COMMENT, + BLOCK_COMMENT, + TERMINAL, + MODIFIER, + IDENTIFIER, + STRING_CHARS, + STRING_ESCAPE, + INT_LITERAL_EXPR, + FLOAT_LITERAL_EXPR, + BOOL_LITERAL_EXPR, + THIS_EXPR, + OUTER_EXPR, + MODULE_EXPR, + NULL_EXPR, + MODULE_TYPE, + UNKNOWN_TYPE, + NOTHING_TYPE, + SHEBANG, + OPERATOR -> + new Text(node.text(source)); + case STRING_NEWLINE -> mustForceLine(); + case MODULE_DECLARATION -> formatModuleDeclaration(node); + case MODULE_DEFINITION -> formatModuleDefinition(node); + case SINGLE_LINE_STRING_LITERAL_EXPR -> formatSingleLineString(node); + case MULTI_LINE_STRING_LITERAL_EXPR -> formatMultilineString(node); + case ANNOTATION -> formatAnnotation(node); + case TYPEALIAS -> formatTypealias(node); + case TYPEALIAS_HEADER -> formatTypealiasHeader(node); + case TYPEALIAS_BODY -> formatTypealiasBody(node); + case MODIFIER_LIST -> formatModifierList(node); + case PARAMETER_LIST, CONSTRAINED_TYPE_CONSTRAINT, FUNCTION_TYPE_PARAMETERS -> + formatParameterList(node, null); + case PARAMETER_LIST_ELEMENTS, + TYPE_PARAMETER_LIST_ELEMENTS, + TYPE_ARGUMENT_LIST_ELEMENTS, + CONSTRAINED_TYPE_ELEMENTS -> + formatParameterListElements(node); + case TYPE_PARAMETER_LIST, TYPE_ARGUMENT_LIST -> formatTypeParameterList(node); + case TYPE_PARAMETER, IMPORT_ALIAS -> + new Group(newId(), formatGeneric(node.children, spaceOrLine())); + case PARAMETER -> formatParameter(node); + case EXTENDS_CLAUSE, AMENDS_CLAUSE -> formatAmendsExtendsClause(node); + case IMPORT_LIST -> formatImportList(node); + case IMPORT -> formatImport(node); + case CLASS -> formatClass(node); + case CLASS_HEADER -> formatClassHeader(node); + case CLASS_HEADER_EXTENDS -> formatClassHeaderExtends(node); + case CLASS_BODY -> formatClassBody(node); + case CLASS_BODY_ELEMENTS -> formatClassBodyElements(node); + case CLASS_PROPERTY, OBJECT_PROPERTY, OBJECT_ENTRY -> formatClassProperty(node); + case CLASS_PROPERTY_HEADER, OBJECT_PROPERTY_HEADER -> formatClassPropertyHeader(node); + case CLASS_PROPERTY_HEADER_BEGIN, OBJECT_PROPERTY_HEADER_BEGIN -> + formatClassPropertyHeaderBegin(node); + case CLASS_PROPERTY_BODY, OBJECT_PROPERTY_BODY -> formatClassPropertyBody(node); + case CLASS_METHOD, OBJECT_METHOD -> formatClassMethod(node); + case CLASS_METHOD_HEADER -> formatClassMethodHeader(node); + case CLASS_METHOD_BODY -> formatClassMethodBody(node); + case OBJECT_BODY -> formatObjectBody(node); + case OBJECT_ELEMENT -> format(node.children.get(0)); // has a single element + case OBJECT_ENTRY_HEADER -> formatObjectEntryHeader(node); + case FOR_GENERATOR -> formatForGenerator(node); + case FOR_GENERATOR_HEADER -> formatForGeneratorHeader(node); + case FOR_GENERATOR_HEADER_DEFINITION -> formatForGeneratorHeaderDefinition(node); + case FOR_GENERATOR_HEADER_DEFINITION_HEADER -> formatForGeneratorHeaderDefinitionHeader(node); + case WHEN_GENERATOR -> formatWhenGenerator(node); + case WHEN_GENERATOR_HEADER -> formatWhenGeneratorHeader(node); + case MEMBER_PREDICATE -> formatMemberPredicate(node); + case QUALIFIED_IDENTIFIER -> formatQualifiedIdentifier(node); + case ARGUMENT_LIST -> formatArgumentList(node, false); + case ARGUMENT_LIST_ELEMENTS -> formatArgumentListElements(node, false, false); + case OBJECT_PARAMETER_LIST -> formatObjectParameterList(node); + case IF_EXPR -> formatIf(node); + case IF_HEADER -> formatIfHeader(node); + case IF_CONDITION -> formatIfCondition(node); + case IF_CONDITION_EXPR -> new Indent(formatGeneric(node.children, (FormatNode) null)); + case IF_THEN_EXPR -> formatIfThen(node); + case IF_ELSE_EXPR -> formatIfElse(node); + case NEW_EXPR, AMENDS_EXPR -> formatNewExpr(node); + case NEW_HEADER -> formatNewHeader(node); + case UNQUALIFIED_ACCESS_EXPR -> formatUnqualifiedAccessExpression(node); + case QUALIFIED_ACCESS_EXPR -> formatQualifiedAccessExpression(node); + case BINARY_OP_EXPR -> formatBinaryOpExpr(node); + case FUNCTION_LITERAL_EXPR -> formatFunctionLiteralExpr(node); + case FUNCTION_LITERAL_BODY -> formatFunctionLiteralBody(node); + case SUBSCRIPT_EXPR, SUPER_SUBSCRIPT_EXPR -> formatSubscriptExpr(node); + case TRACE_EXPR, THROW_EXPR, READ_EXPR -> formatTraceThrowReadExpr(node); + case PARENTHESIZED_EXPR -> formatParenthesizedExpr(node); + case PARENTHESIZED_EXPR_ELEMENTS -> formatParenthesizedExprElements(node); + case LET_EXPR -> formatLetExpr(node); + case LET_PARAMETER_DEFINITION -> formatLetParameterDefinition(node); + case LET_PARAMETER -> formatLetParameter(node); + case TYPE_ANNOTATION -> formatTypeAnnotation(node); + case DECLARED_TYPE -> formatDeclaredType(node); + case CONSTRAINED_TYPE -> formatConstrainedType(node); + case UNION_TYPE -> formatUnionType(node); + case FUNCTION_TYPE -> formatFunctionType(node); + case STRING_CONSTANT_TYPE -> format(node.children.get(0)); + case PARENTHESIZED_TYPE -> formatParenthesizedType(node); + case PARENTHESIZED_TYPE_ELEMENTS -> formatParenthesizedTypeElements(node); + default -> throw new RuntimeException("Unknown node type: " + node.type); + }; + } + + private FormatNode formatModule(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> linesBetween(prev, next) > 1 ? TWO_NEWLINES : forceLine()); + return new Nodes(nodes); + } + + private FormatNode formatModuleDeclaration(Node node) { + return new Nodes(formatGeneric(node.children, TWO_NEWLINES)); + } + + private FormatNode formatModuleDefinition(Node node) { + var split = splitPrefixes(node.children); + var prefixes = split[0]; + var rest = split[1]; + var fnodes = + formatGenericWithGen( + rest, + Space.INSTANCE, + (n, next) -> + n.type == NodeType.QUALIFIED_IDENTIFIER + ? new Nodes(formatGeneric(n.children, (FormatNode) null)) + : format(n)); + var res = new Nodes(fnodes); + if (prefixes.isEmpty()) { + return res; + } + var sep = getSeparator(prefixes.get(prefixes.size() - 1), rest.get(0), spaceOrLine()); + var result = new ArrayList<>(formatGeneric(prefixes, spaceOrLine())); + result.add(sep); + result.add(res); + return new Nodes(result); + } + + private FormatNode formatDocComment(Node node) { + var txt = text(node); + if (txt.equals("///") || txt.equals("/// ")) return new Text("///"); + + var comment = txt.substring(3); + if (isStrictBlank(comment)) return new Text("///"); + + if (!comment.isEmpty() && comment.charAt(0) != ' ') comment = " " + comment; + return new Text("///" + comment); + } + + private static boolean isStrictBlank(String s) { + for (var i = 0; i < s.length(); i++) { + var ch = s.charAt(i); + if (ch != ' ' && ch != '\t') return false; + } + return true; + } + + private FormatNode formatQualifiedIdentifier(Node node) { + // short circuit + if (node.children.size() == 1) return format(node.children.get(0)); + + var first = new ArrayList(); + first.add(format(node.children.get(0))); + first.add(line()); + var rest = node.children.subList(1, node.children.size()); + var nodes = formatGeneric(rest, (n1, next) -> n1.type == NodeType.TERMINAL ? null : line()); + first.add(new Indent(nodes)); + return new Group(newId(), first); + } + + private FormatNode formatUnqualifiedAccessExpression(Node node) { + var children = node.children; + if (children.size() == 1) return format(children.get(0)); + var firstNode = firstProperChild(node); + if (firstNode != null && text(firstNode).equals("Map")) { + var nodes = + formatGenericWithGen( + children, + (FormatNode) null, + (n, next) -> + n.type == NodeType.ARGUMENT_LIST ? formatArgumentList(n, true) : format(n)); + return new Nodes(nodes); + } + return new Nodes(formatGeneric(children, (FormatNode) null)); + } + + /** + * Special cases when formatting qualified access: + * + *

Case 1: Dot calls followed by closing method call: wrap after the opening paren. + * + *

+   * {@code foo.bar.baz(
+   *   new { qux = 1 }
+   * )}
+   * 
+ * + *

Case 2: Dot calls, then method calls: group the leading access together. + * + *

+   * {@code foo.bar
+   *  .baz(new { qux = 1 })
+   *  .baz()}
+   * 
+ * + *

Case 3: If there are multiple lambdas present, always force a newline. + * + *

+   * {@code foo
+   *   .map((it) -> it + 1)
+   *   .filter((it) -> it.isEven)}
+   * 
+ */ + private FormatNode formatQualifiedAccessExpression(Node node) { + var lambdaCount = new int[] {0}; + var methodCallCount = new int[] {0}; + var indexBeforeFirstMethodCall = new int[] {0}; + var flat = new ArrayList(); + + gatherFacts(node, flat, lambdaCount, methodCallCount, indexBeforeFirstMethodCall); + + BiFunction leadingSeparator = + (prev, next) -> { + if (prev.type == NodeType.OPERATOR) return null; + if (next.type == NodeType.OPERATOR) return line(); + return spaceOrLine(); + }; + + BiFunction trailingSeparator = + (prev, next) -> { + if (prev.type == NodeType.OPERATOR) return null; + if (next.type == NodeType.OPERATOR) return lambdaCount[0] > 1 ? forceLine() : line(); + return spaceOrLine(); + }; + + List nodes; + if (methodCallCount[0] == 1 && isMethodCall(lastProperNode(flat))) { + // lift argument list into its own node + var splitResult = splitFunctionCallNode(flat); + var callChain = splitResult[0]; + var argsList = splitResult[1]; + var leadingNodes = indentAfterFirstNewline(formatGeneric(callChain, leadingSeparator), true); + var trailingNodes = formatGeneric(argsList, trailingSeparator); + var sep = getBaseSeparator(callChain.get(callChain.size() - 1), argsList.get(0)); + if (sep != null) { + nodes = concat(leadingNodes, sep, trailingNodes); + } else { + nodes = concat(leadingNodes, trailingNodes); + } + } else if (methodCallCount[0] > 0 && indexBeforeFirstMethodCall[0] > 0) { + var leading = flat.subList(0, indexBeforeFirstMethodCall[0]); + var trailing = flat.subList(indexBeforeFirstMethodCall[0], flat.size()); + var leadingNodes = indentAfterFirstNewline(formatGeneric(leading, leadingSeparator), true); + var trailingNodes = formatGeneric(trailing, trailingSeparator); + nodes = concat(leadingNodes, line(), trailingNodes); + } else { + nodes = formatGeneric(flat, trailingSeparator); + } + + var shouldGroup = node.children.size() == flat.size(); + return new Group(newId(), indentAfterFirstNewline(nodes, shouldGroup)); + } + + private void gatherFacts( + Node current, + List flat, + int[] lambdaCount, + int[] methodCallCount, + int[] indexBeforeFirstMethodCall) { + for (var child : current.children) { + if (child.type == NodeType.QUALIFIED_ACCESS_EXPR) { + gatherFacts(child, flat, lambdaCount, methodCallCount, indexBeforeFirstMethodCall); + } else { + flat.add(child); + if (isMethodCall(child)) { + methodCallCount[0]++; + if (hasFunctionLiteral(child, 2)) { + lambdaCount[0]++; + } + } else if (methodCallCount[0] == 0) { + indexBeforeFirstMethodCall[0] = flat.size() - 1; + } + } + } + } + + /** + * Split a function call node to extract its identifier into the leading group. For example, + * {@code foo.bar(5)} becomes: leading gets {@code foo.bar}, rest gets {@code (5)}. + */ + private List[] splitFunctionCallNode(List nodes) { + assert !nodes.isEmpty(); + var lastNode = nodes.get(nodes.size() - 1); + var argListIdx = -1; + for (var i = 0; i < lastNode.children.size(); i++) { + if (lastNode.children.get(i).type == NodeType.ARGUMENT_LIST) { + argListIdx = i; + break; + } + } + var leading = new ArrayList<>(nodes.subList(0, nodes.size() - 1)); + leading.addAll(lastNode.children.subList(0, argListIdx)); + var trailing = lastNode.children.subList(argListIdx, lastNode.children.size()); + //noinspection unchecked + return new List[] {leading, trailing}; + } + + private static boolean isMethodCall(Node node) { + if (node == null || node.type != NodeType.UNQUALIFIED_ACCESS_EXPR) return false; + for (var child : node.children) { + if (child.type == NodeType.ARGUMENT_LIST) return true; + } + return false; + } + + private FormatNode formatAmendsExtendsClause(Node node) { + var prefix = formatGeneric(node.children.subList(0, node.children.size() - 1), spaceOrLine()); + // string constant + var suffix = new Indent(List.of(format(node.children.get(node.children.size() - 1)))); + var result = new ArrayList<>(prefix); + result.add(spaceOrLine()); + result.add(suffix); + return new Group(newId(), result); + } + + private FormatNode formatImport(Node node) { + return new Group( + newId(), + formatGenericWithGen( + node.children, + spaceOrLine(), + (n, next) -> isTerminal(n, "import") ? format(n) : indent(format(n)))); + } + + private FormatNode formatAnnotation(Node node) { + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatTypealias(Node node) { + var nodes = + groupNonPrefixes( + node, children -> new Group(newId(), formatGeneric(children, spaceOrLine()))); + return new Nodes(nodes); + } + + private FormatNode formatTypealiasHeader(Node node) { + return new Group(newId(), formatGeneric(node.children, Space.INSTANCE)); + } + + private FormatNode formatTypealiasBody(Node node) { + return new Indent(formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatClass(Node node) { + return new Nodes(formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatClassHeader(Node node) { + return groupOnSpace(formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatClassHeaderExtends(Node node) { + return indent(new Group(newId(), formatGeneric(node.children, spaceOrLine()))); + } + + private FormatNode formatClassBody(Node node) { + var children = node.children; + if (children.size() == 2) { + // no members + return new Nodes(formatGeneric(children, (FormatNode) null)); + } + return new Group(newId(), formatGeneric(children, forceLine())); + } + + private FormatNode formatClassBodyElements(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> { + var lineDiff = linesBetween(prev, next); + return (lineDiff > 1 || lineDiff == 0) ? TWO_NEWLINES : forceLine(); + }); + return new Indent(nodes); + } + + private FormatNode formatClassProperty(Node node) { + Node lastExprOrBody = null; + for (var i = node.children.size() - 1; i >= 0; i--) { + var child = node.children.get(i); + if (isExpressionOrPropertyBody(child)) { + lastExprOrBody = child; + break; + } + } + var sameLine = false; + if (lastExprOrBody != null) { + sameLine = + lastExprOrBody.type.isExpression() + ? isSameLineExpr(lastExprOrBody) + : isSameLineExpr(lastExprOrBody.children.get(lastExprOrBody.children.size() - 1)); + } + var sameLineFinal = sameLine; + var nodes = + groupNonPrefixes( + node, + children -> + groupOnSpace( + formatGenericWithGen( + children, + (prev, next) -> sameLineFinal ? Space.INSTANCE : spaceOrLine(), + (n, next) -> + isExpressionOrPropertyBody(n) && !sameLineFinal + ? indent(format(n)) + : format(n)))); + return new Nodes(nodes); + } + + private static boolean isExpressionOrPropertyBody(Node node) { + return node.type.isExpression() + || node.type == NodeType.CLASS_PROPERTY_BODY + || node.type == NodeType.OBJECT_PROPERTY_BODY; + } + + private FormatNode formatClassPropertyHeader(Node node) { + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatClassPropertyHeaderBegin(Node node) { + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatClassPropertyBody(Node node) { + return new Nodes(formatGeneric(node.children, (FormatNode) null)); + } + + private FormatNode formatClassMethod(Node node) { + var prefixes = new ArrayList(); + List methodNodes; + if (node.children.get(0).type == NodeType.CLASS_METHOD_HEADER) { + methodNodes = node.children; + } else { + var idx = -1; + for (var i = 0; i < node.children.size(); i++) { + if (node.children.get(i).type == NodeType.CLASS_METHOD_HEADER) { + idx = i; + break; + } + } + var prefixNodes = node.children.subList(0, idx); + prefixes.addAll(formatGeneric(prefixNodes, (FormatNode) null)); + prefixes.add( + getSeparator( + prefixNodes.get(prefixNodes.size() - 1), node.children.get(idx), forceLine())); + methodNodes = node.children.subList(idx, node.children.size()); + } + + // Separate header (before =) and body (= and after) + var bodyIdx = -1; + for (var i = 0; i < methodNodes.size(); i++) { + if (methodNodes.get(i).type == NodeType.CLASS_METHOD_BODY) { + bodyIdx = i - 1; + break; + } + } + var header = bodyIdx < 0 ? methodNodes : methodNodes.subList(0, bodyIdx); + var headerGroupId = newId(); + var methodGroupId = newId(); + var headerNodes = + formatGenericWithGen( + header, + spaceOrLine(), + (n, next) -> + n.type == NodeType.PARAMETER_LIST + ? formatParameterList(n, headerGroupId) + : format(n)); + if (bodyIdx < 0) { + // body is Empty(), return header + if (prefixes.isEmpty()) { + return new Group(headerGroupId, headerNodes); + } + prefixes.add(new Group(headerGroupId, headerNodes)); + return new Nodes(prefixes); + } + + var bodyNodes = methodNodes.subList(bodyIdx, methodNodes.size()); + var expr = bodyNodes.get(bodyNodes.size() - 1).children.get(0); + var isSameLineBody = isSameLineExpr(expr); + + // Format body (= and expression) + List bodyFormat; + if (isSameLineBody) { + bodyFormat = formatGeneric(bodyNodes, Space.INSTANCE); + } else { + bodyFormat = + formatGenericWithGen( + bodyNodes, spaceOrLine(), (n, next) -> next == null ? indent(format(n)) : format(n)); + } + + var headerGroup = new Group(headerGroupId, headerNodes); + var bodyGroup = new Group(newId(), bodyFormat); + var separator = getSeparator(header.get(header.size() - 1), bodyNodes.get(0), Space.INSTANCE); + var allNodes = new Group(methodGroupId, List.of(headerGroup, separator, bodyGroup)); + + if (prefixes.isEmpty()) return allNodes; + prefixes.add(allNodes); + return new Nodes(prefixes); + } + + private FormatNode formatClassMethodHeader(Node node) { + return new Nodes(formatGeneric(node.children, Space.INSTANCE)); + } + + private FormatNode formatClassMethodBody(Node node) { + return new Group(newId(), formatGeneric(node.children, (FormatNode) null)); + } + + private FormatNode formatParameter(Node node) { + if (node.children.size() == 1) return format(node.children.get(0)); // underscore + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatParameterList(Node node, Integer id) { + if (node.children.size() == 2) return new Text("()"); + var groupId = id != null ? id : newId(); + var nodes = + formatGeneric( + node.children, + (prev, next) -> { + if (isTerminal(prev, "(") || isTerminal(next, ")")) { + if (isTerminal(next, ")")) { + // trailing comma + if (grammarVersion == GrammarVersion.V1) { + return line(); + } else { + return ifWrap(groupId, nodes(new Text(","), line()), line()); + } + } + return line(); + } + return spaceOrLine(); + }); + return id != null ? new Nodes(nodes) : new Group(groupId, nodes); + } + + private FormatNode formatArgumentList(Node node, boolean twoBy2) { + if (node.children.size() == 2) return new Text("()"); + var hasTrailingLambda = hasTrailingLambda(node); + var groupId = newId(); + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> { + if (isTerminal(prev, "(") || isTerminal(next, ")")) { + var lineNode = hasTrailingLambda ? Empty.INSTANCE : line(); + if (isTerminal(next, ")") && !hasTrailingLambda) { + // trailing comma + if (grammarVersion == GrammarVersion.V1) { + return lineNode; + } else { + return ifWrap(groupId, nodes(new Text(","), lineNode), lineNode); + } + } + return lineNode; + } + return spaceOrLine(); + }, + (n, next) -> + n.type == NodeType.ARGUMENT_LIST_ELEMENTS + ? formatArgumentListElements(n, hasTrailingLambda, twoBy2) + : format(n)); + return new Group(groupId, nodes); + } + + private FormatNode formatArgumentListElements( + Node node, boolean hasTrailingLambda, boolean twoBy2) { + var children = node.children; + var shouldMultiline = shouldMultilineNodes(node, n -> isTerminal(n, ",")); + BiFunction sep = + (prev, next) -> shouldMultiline ? forceSpaceyLine() : spaceOrLine(); + if (twoBy2) { + var pairs = pairArguments(children); + var nodes = + formatGenericWithGen( + pairs, + sep, + (n, next) -> + n.type == NodeType.ARGUMENT_LIST_ELEMENTS + ? new Group(newId(), formatGeneric(n.children, spaceOrLine())) + : format(n)); + return new Indent(nodes); + } + if (hasTrailingLambda) { + // if the args have a trailing lambda, group them differently + var splitIndex = -1; + for (var i = children.size() - 1; i >= 0; i--) { + if (SAME_LINE_EXPRS.contains(children.get(i).type)) { + splitIndex = i; + break; + } + } + var normalParams = children.subList(0, splitIndex); + var lastParam = children.subList(splitIndex, children.size()); + var trailingNode = + endsWithClosingCurlyBrace(lastParam.get(lastParam.size() - 1)) ? Empty.INSTANCE : line(); + var lastNodes = formatGenericWithGen(lastParam, sep, null); + if (normalParams.isEmpty()) { + return group(new Group(newId(), lastNodes), trailingNode); + } + var separator = + getSeparator(normalParams.get(normalParams.size() - 1), lastParam.get(0), Space.INSTANCE); + var paramNodes = formatGenericWithGen(normalParams, sep, null); + return group( + new Group(newId(), paramNodes), separator, new Group(newId(), lastNodes), trailingNode); + } + return new Indent(formatGeneric(children, sep)); + } + + private boolean shouldMultilineNodes(Node node, Predicate predicate) { + for (var idx = 0; idx < node.children.size() - 1; idx++) { + var prev = node.children.get(idx); + var next = node.children.get(idx + 1); + if ((predicate.test(prev) || predicate.test(next)) && linesBetween(prev, next) > 0) { + return true; + } + } + return false; + } + + private boolean endsWithClosingCurlyBrace(Node node) { + while (!node.children.isEmpty()) { + node = node.children.get(node.children.size() - 1); + } + return isTerminalSingle(node, "}"); + } + + /** + * Tells if an argument list has a trailing lambda, new expr, or amends expr. + * + *

Only considered trailing lamdba if: 1. There is only one lambda/new expr/amends expr in the + * list. E.g. avoid formatting `toMap()` weirdly: ``` foo.toMap( (it) -> makeSomeKey(it), (it) -> + * makeSomeValue(it), ) ``` 2. The lambda does not have leading or trailing line comment. + */ + private boolean hasTrailingLambda(Node argList) { + var elementsNode = firstProperChild(argList); + if (elementsNode == null) return false; + var children = elementsNode.children; + var seenLambda = false; + if (children.get(children.size() - 1).type == NodeType.LINE_COMMENT) return false; + for (var i = children.size() - 1; i >= 0; i--) { + var child = children.get(i); + if (!isProper(child)) continue; + if (!seenLambda) { + if (!SAME_LINE_EXPRS.contains(child.type)) return false; + // preceded by Line() comment + if (i > 0 && children.get(i - 1).type == NodeType.LINE_COMMENT) return false; + seenLambda = true; + } else if (SAME_LINE_EXPRS.contains(child.type)) { + return false; + } + } + return true; + } + + private List pairArguments(List nodes) { + var res = new ArrayList(); + var tmp = new ArrayList(); + var commas = 0; + for (var node : nodes) { + if (isTerminalSingle(node, ",")) { + commas++; + if (commas == 2) { + var suffixes = new ArrayList(); + while (!tmp.isEmpty() && tmp.get(tmp.size() - 1).type.isAffix()) { + // trailing comments should not be paired + suffixes.add(tmp.remove(tmp.size() - 1)); + } + res.add(new Node(NodeType.ARGUMENT_LIST_ELEMENTS, tmp)); + for (var j = suffixes.size() - 1; j >= 0; j--) { + res.add(suffixes.get(j)); + } + res.add(node); + commas = 0; + tmp = new ArrayList<>(); + } else { + tmp.add(node); + } + } else if (tmp.isEmpty() && node.type.isAffix()) { + // leading comments should not be paired + res.add(node); + } else { + tmp.add(node); + } + } + if (!tmp.isEmpty()) { + res.add(new Node(NodeType.ARGUMENT_LIST_ELEMENTS, tmp)); + } + return res; + } + + private FormatNode formatParameterListElements(Node node) { + return new Indent(formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatTypeParameterList(Node node) { + if (node.children.size() == 2) return new Text("<>"); + var groupId = newId(); + var nodes = + formatGeneric( + node.children, + (prev, next) -> { + if (isTerminal(prev, "<") || isTerminal(next, ">")) { + if (isTerminal(next, ">")) { + // trailing comma + if (grammarVersion == GrammarVersion.V1) { + return new Line(); + } else { + return ifWrap(groupId, nodes(new Text(","), line()), line()); + } + } + return line(); + } + return spaceOrLine(); + }); + return new Group(groupId, nodes); + } + + private FormatNode formatObjectParameterList(Node node) { + // object param lists don't have trailing commas, as they have a trailing -> + var groupId = newId(); + var nonWrappingNodes = new Nodes(formatGeneric(node.children, spaceOrLine())); + // double indent the params if they wrap + var wrappingNodes = indent(new Indent(concat(List.of(line()), List.of(nonWrappingNodes)))); + return new Group( + groupId, List.of(ifWrap(groupId, wrappingNodes, nodes(Space.INSTANCE, nonWrappingNodes)))); + } + + private FormatNode formatObjectBody(Node node) { + if (node.children.size() == 2) return new Text("{}"); + var groupId = newId(); + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> { + if (next.type == NodeType.OBJECT_PARAMETER_LIST) return Empty.INSTANCE; + if (isTerminal(prev, "{") || isTerminal(next, "}")) { + var lines = linesBetween(prev, next); + return lines == 0 ? spaceOrLine() : forceSpaceyLine(); + } + return spaceOrLine(); + }, + (n, next) -> + n.type == NodeType.OBJECT_MEMBER_LIST + ? formatObjectMemberList(n, groupId) + : format(n)); + return new Group(groupId, nodes); + } + + private FormatNode formatObjectMemberList(Node node, int groupId) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> { + var lines = linesBetween(prev, next); + if (lines == 0) return ifWrap(groupId, line(), new Text("; ")); + if (lines == 1) return forceLine(); + return TWO_NEWLINES; + }); + return new Indent(nodes); + } + + private FormatNode formatObjectEntryHeader(Node node) { + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatForGenerator(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> + prev.type == NodeType.FOR_GENERATOR_HEADER + || next.type == NodeType.FOR_GENERATOR_HEADER + ? Space.INSTANCE + : spaceOrLine()); + return new Group(newId(), nodes); + } + + private FormatNode formatForGeneratorHeader(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : null); + return new Group(newId(), nodes); + } + + private FormatNode formatForGeneratorHeaderDefinition(Node node) { + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> SAME_LINE_EXPRS.contains(next.type) ? Space.INSTANCE : spaceOrLine(), + (n, next) -> + n.type.isExpression() && !SAME_LINE_EXPRS.contains(n.type) + ? indent(format(n)) + : format(n)); + return indent(new Group(newId(), nodes)); + } + + private FormatNode formatForGeneratorHeaderDefinitionHeader(Node node) { + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatWhenGenerator(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> + prev.type == NodeType.WHEN_GENERATOR_HEADER + || isTerminal(prev, "when", "else") + || isTerminal(next, "else") + ? Space.INSTANCE + : spaceOrLine()); + return new Group(newId(), nodes); + } + + private FormatNode formatWhenGeneratorHeader(Node node) { + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine(), + (n, next) -> + !n.type.isAffix() && n.type != NodeType.TERMINAL ? indent(format(n)) : format(n)); + return new Group(newId(), nodes); + } + + private FormatNode formatMemberPredicate(Node node) { + var nodes = + formatGenericWithGen( + node.children, + spaceOrLine(), + (n, next) -> + next == null && n.type != NodeType.OBJECT_BODY ? indent(format(n)) : format(n)); + return new Group(newId(), nodes); + } + + private List formatStringParts(List nodes) { + var result = new ArrayList(); + var isInStringInterpolation = false; + var cursor = new PeekableIterator<>(nodes.iterator()); + Node prev = null; + while (cursor.hasNext()) { + if (isInStringInterpolation) { + var prevNoNewlines = noNewlines; + var elems = cursor.takeUntilBefore(n -> isTerminalSingle(n, ")")); + noNewlines = !isMultilineList(elems); + var baseSep = getBaseSeparator(prev, elems.get(0)); + if (baseSep != null) result.add(baseSep); + result.addAll(formatGeneric(elems, (FormatNode) null)); + var endSep = getBaseSeparator(elems.get(elems.size() - 1), cursor.peek()); + if (endSep != null) result.add(endSep); + noNewlines = prevNoNewlines; + isInStringInterpolation = false; + continue; + } + var elem = cursor.next(); + if (elem.type == NodeType.TERMINAL && text(elem).endsWith("(")) { + isInStringInterpolation = true; + } + result.add(format(elem)); + prev = elem; + } + return result; + } + + private FormatNode formatSingleLineString(Node node) { + return new Group(newId(), formatStringParts(node.children)); + } + + private FormatNode formatMultilineString(Node node) { + var nodes = formatStringParts(node.children); + return new MultilineStringGroup( + node.children.get(node.children.size() - 1).span.colBegin(), nodes); + } + + private FormatNode formatIf(Node node) { + var separator = isMultiline(node) ? forceSpaceyLine() : spaceOrLine(); + var nodes = + formatGeneric( + node.children, + (prev, next) -> { + // produce `else if` in the case of nested if. + // note: don't need to handle if `next.children[0]` is an affix because that can't be + // emitted as `else if` anyway. + if (next.type == NodeType.IF_ELSE_EXPR + && next.children.get(0).type == NodeType.IF_EXPR) { + return Space.INSTANCE; + } + return separator; + }); + return new Group(newId(), nodes); + } + + private FormatNode formatIfHeader(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> next.type == NodeType.IF_CONDITION ? Space.INSTANCE : spaceOrLine()); + return new Group(newId(), nodes); + } + + private FormatNode formatIfCondition(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> + isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine()); + return new Group(newId(), nodes); + } + + private FormatNode formatIfThen(Node node) { + return new Indent(formatGeneric(node.children, (FormatNode) null)); + } + + private FormatNode formatIfElse(Node node) { + var children = node.children; + if (children.size() == 1) { + var expr = children.get(0); + if (expr.type == NodeType.IF_EXPR) { + // unpack the group + var group = (Group) formatIf(expr); + return new Nodes(group.nodes()); + } + return indent(format(expr)); + } + return new Indent(formatGeneric(node.children, (FormatNode) null)); + } + + private FormatNode formatNewExpr(Node node) { + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatNewHeader(Node node) { + return new Group(newId(), formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatParenthesizedExpr(Node node) { + if (node.children.size() == 2) return new Text("()"); + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine(), + (n, next) -> n.type.isExpression() ? indent(format(n)) : format(n)); + return new Group(newId(), nodes); + } + + private FormatNode formatParenthesizedExprElements(Node node) { + return indent(new Group(newId(), formatGeneric(node.children, (FormatNode) null))); + } + + private FormatNode formatFunctionLiteralExpr(Node node) { + var splitResult = splitOn(node.children, n -> isTerminalSingle(n, "->")); + var params = splitResult[0]; + var rest = splitResult[1]; + Node bodyNode = null; + for (var child : node.children) { + if (child.type == NodeType.FUNCTION_LITERAL_BODY) { + bodyNode = child; + break; + } + } + Node exprNode = null; + assert bodyNode != null; + for (var child : bodyNode.children) { + if (child.type.isExpression()) { + exprNode = child; + break; + } + } + assert exprNode != null; + var sameLine = isSameLineExpr(exprNode); + var sep = sameLine ? Space.INSTANCE : spaceOrLine(); + var bodySep = getSeparator(params.get(params.size() - 1), rest.get(0), sep); + + var nodes = formatGeneric(params, sep); + var restNodes = new ArrayList(); + restNodes.add(bodySep); + restNodes.addAll(formatGeneric(rest, sep)); + var result = new ArrayList<>(nodes); + result.add(new Group(newId(), restNodes)); + return new Group(newId(), result); + } + + private FormatNode formatFunctionLiteralBody(Node node) { + Node expr = null; + for (var child : node.children) { + if (child.type.isExpression()) { + expr = child; + break; + } + } + var nodes = formatGeneric(node.children, (FormatNode) null); + assert expr != null; + return isSameLineExpr(expr) ? new Group(newId(), nodes) : new Indent(nodes); + } + + private FormatNode formatLetExpr(Node node) { + var separator = isMultiline(node) ? forceSpaceyLine() : spaceOrLine(); + var endsWithLet = node.children.get(node.children.size() - 1).type == NodeType.LET_EXPR; + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> + next.type == NodeType.LET_PARAMETER_DEFINITION ? Space.INSTANCE : separator, + (n, next) -> { + if (n.type == NodeType.LET_EXPR) { + // unpack the lets + var group = (Group) formatLetExpr(n); + return new Nodes(group.nodes()); + } + if (endsWithLet) return format(n); + if (n.type.isExpression() || n.type.isAffix()) return indent(format(n)); + return format(n); + }); + return new Group(newId(), nodes); + } + + private FormatNode formatLetParameterDefinition(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> + isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine()); + return new Group(newId(), nodes); + } + + private FormatNode formatLetParameter(Node node) { + return indent(formatClassProperty(node)); + } + + private FormatNode formatBinaryOpExpr(Node node) { + var flat = flattenBinaryOperatorExprs(node); + var shouldMultiline = shouldMultilineNodes(node, n -> n.type == NodeType.OPERATOR); + var nodes = + formatGeneric( + flat, + (prev, next) -> { + var sep = shouldMultiline ? forceSpaceyLine() : spaceOrLine(); + if (prev.type == NodeType.OPERATOR) { + return text(prev).equals("-") ? sep : Space.INSTANCE; + } + if (next.type == NodeType.OPERATOR) { + return text(next).equals("-") ? Space.INSTANCE : sep; + } + return sep; + }); + + var shouldGroup = node.children.size() == flat.size(); + return new Group(newId(), indentAfterFirstNewline(nodes, shouldGroup)); + } + + private static boolean hasFunctionLiteral(Node node, int depth) { + if (node.type == NodeType.FUNCTION_LITERAL_EXPR) return true; + for (var child : node.children) { + if (child.type == NodeType.FUNCTION_LITERAL_EXPR) return true; + if (depth > 0 && hasFunctionLiteral(child, depth - 1)) return true; + } + return false; + } + + private FormatNode formatSubscriptExpr(Node node) { + return new Nodes(formatGeneric(node.children, (FormatNode) null)); + } + + private FormatNode formatTraceThrowReadExpr(Node node) { + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : null, + (n, next) -> n.type.isExpression() ? indent(format(n)) : format(n)); + return new Group(newId(), nodes); + } + + private FormatNode formatDeclaredType(Node node) { + return new Nodes(formatGeneric(node.children, spaceOrLine())); + } + + private FormatNode formatConstrainedType(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> + next.type == NodeType.CONSTRAINED_TYPE_CONSTRAINT ? null : spaceOrLine()); + return new Group(newId(), nodes); + } + + private FormatNode formatUnionType(Node node) { + var nodes = + formatGeneric( + node.children, + (prev, next) -> { + if (isTerminal(next, "|")) return spaceOrLine(); + if (isTerminal(prev, "|")) return Space.INSTANCE; + return null; + }); + return new Group(newId(), indentAfterFirstNewline(nodes, false)); + } + + private FormatNode formatFunctionType(Node node) { + var nodes = + formatGenericWithGen( + node.children, + (prev, next) -> isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine(), + (n, next) -> next == null ? indent(format(n)) : format(n)); + return new Group(newId(), nodes); + } + + private FormatNode formatParenthesizedType(Node node) { + if (node.children.size() == 2) return new Text("()"); + var groupId = newId(); + var nodes = + formatGeneric( + node.children, + (prev, next) -> + isTerminal(prev, "(") || isTerminal(next, ")") ? line() : spaceOrLine()); + return new Group(groupId, nodes); + } + + private FormatNode formatParenthesizedTypeElements(Node node) { + return indent(new Group(newId(), formatGeneric(node.children, spaceOrLine()))); + } + + private FormatNode formatTypeAnnotation(Node node) { + return new Group(newId(), formatGeneric(node.children, Space.INSTANCE)); + } + + private FormatNode formatModifierList(Node node) { + var result = new ArrayList(); + var affixes = new ArrayList(); + var modifiers = new ArrayList(); + for (var child : node.children) { + if (child.type.isAffix()) { + affixes.add(child); + } else { + modifiers.add(child); + } + } + if (!affixes.isEmpty()) { + result.addAll(formatGeneric(affixes, spaceOrLine())); + } + modifiers.sort((a, b) -> modifierPrecedence(a) - modifierPrecedence(b)); + result.addAll(formatGeneric(modifiers, Space.INSTANCE)); + return new Nodes(result); + } + + private record ImportWithComments( + List leadingAffixes, Node importNode, List trailingAffixes) {} + + private List buildImportsWithComments(List children) { + var result = new ArrayList(); + var lastImport = (Node) null; + var lastTrailing = new ArrayList(); + var lastLeading = new ArrayList(); + var pendingAffixes = new ArrayList(); + + for (var child : children) { + if (child.type.isAffix()) { + if (lastImport != null && lastImport.span.lineEnd() == child.span.lineBegin()) { + // trailing comment on the same Line as the preceding import + lastTrailing.add(child); + } else { + // leading comment for the next import + // first, flush the previous import + if (lastImport != null) { + result.add(new ImportWithComments(lastLeading, lastImport, lastTrailing)); + lastImport = null; + lastTrailing = new ArrayList<>(); + lastLeading = new ArrayList<>(); + } + pendingAffixes.add(child); + } + } else { + // import node + if (lastImport != null) { + result.add(new ImportWithComments(lastLeading, lastImport, lastTrailing)); + lastTrailing = new ArrayList<>(); + } + lastLeading = pendingAffixes; + pendingAffixes = new ArrayList<>(); + lastImport = child; + } + } + // flush the last import + if (lastImport != null) { + result.add(new ImportWithComments(lastLeading, lastImport, lastTrailing)); + } + return result; + } + + private FormatNode formatImportList(Node node) { + var nodes = new ArrayList(); + var allImportsWithComments = buildImportsWithComments(node.children); + + var regularImports = new ArrayList(); + var globImports = new ArrayList(); + for (var entry : allImportsWithComments) { + var terminalNode = entry.importNode.findChildByType(NodeType.TERMINAL); + var terminalText = terminalNode != null ? terminalNode.text(source) : null; + if ("import*".equals(terminalText)) { + globImports.add(entry); + } else { + regularImports.add(entry); + } + } + + if (!regularImports.isEmpty()) { + formatImportListHelper(regularImports, nodes); + if (!globImports.isEmpty()) nodes.add(TWO_NEWLINES); + } + if (!globImports.isEmpty()) { + formatImportListHelper(globImports, nodes); + } + return new Nodes(nodes); + } + + private void formatImportWithComments(ImportWithComments entry, List nodes) { + if (!entry.leadingAffixes.isEmpty()) { + nodes.addAll(formatGeneric(entry.leadingAffixes, spaceOrLine())); + nodes.add(forceLine()); + } + nodes.add(format(entry.importNode)); + for (var affix : entry.trailingAffixes) { + nodes.add(Space.INSTANCE); + nodes.add(format(affix)); + } + } + + private void formatImportListHelper(List allImports, List nodes) { + var comparator = new ImportComparator(source); + var absolute = new ArrayList(); + var projects = new ArrayList(); + var relatives = new ArrayList(); + + for (var entry : allImports) { + var url = getImportUrl(entry.importNode); + if (ABSOLUTE_URL_REGEX.matcher(url).matches()) { + absolute.add(entry); + } else if (url.startsWith("@")) { + projects.add(entry); + } else { + relatives.add(entry); + } + } + + absolute.sort((a, b) -> comparator.compare(a.importNode, b.importNode)); + projects.sort((a, b) -> comparator.compare(a.importNode, b.importNode)); + relatives.sort((a, b) -> comparator.compare(a.importNode, b.importNode)); + + var shouldNewline = false; + if (!absolute.isEmpty()) { + for (var i = 0; i < absolute.size(); i++) { + if (i > 0) nodes.add(forceLine()); + formatImportWithComments(absolute.get(i), nodes); + } + if (!projects.isEmpty() || !relatives.isEmpty()) nodes.add(forceLine()); + shouldNewline = true; + } + if (!projects.isEmpty()) { + if (shouldNewline) nodes.add(forceLine()); + for (var i = 0; i < projects.size(); i++) { + if (i > 0) nodes.add(forceLine()); + formatImportWithComments(projects.get(i), nodes); + } + if (!relatives.isEmpty()) nodes.add(forceLine()); + shouldNewline = true; + } + if (!relatives.isEmpty()) { + if (shouldNewline) nodes.add(forceLine()); + for (var i = 0; i < relatives.size(); i++) { + if (i > 0) nodes.add(forceLine()); + formatImportWithComments(relatives.get(i), nodes); + } + } + } + + // --- formatGeneric overloads --- + + private List formatGeneric(List children, FormatNode separator) { + return formatGeneric(children, (prev, next) -> separator); + } + + private List formatGeneric( + List children, BiFunction separatorFn) { + return formatGenericWithGen(children, separatorFn, null); + } + + private List formatGenericWithGen( + List children, FormatNode separator, BiFunction generatorFn) { + return formatGenericWithGen(children, (prev, next) -> separator, generatorFn); + } + + private List formatGenericWithGen( + List children, + BiFunction separatorFn, + BiFunction generatorFn) { + // skip semicolons + var filtered = new ArrayList(children.size()); + for (var child : children) { + if (!isSemicolon(child)) filtered.add(child); + } + children = filtered; + + // short circuit + if (children.isEmpty()) return List.of(); + if (children.size() == 1) return List.of(format(children.get(0))); + + var nodes = new ArrayList(); + var prev = children.get(0); + for (var i = 1; i < children.size(); i++) { + var child = children.get(i); + nodes.add(generatorFn != null ? generatorFn.apply(prev, child) : format(prev)); + var separator = getSeparator(prev, child, separatorFn); + if (separator != null) nodes.add(separator); + prev = child; + } + nodes.add( + generatorFn != null + ? generatorFn.apply(children.get(children.size() - 1), null) + : format(children.get(children.size() - 1))); + return nodes; + } + + private boolean isSemicolon(Node node) { + return node.type.isAffix() && text(node).equals(";"); + } + + /** Groups all non prefixes (comments, doc comments, annotations) of this node together. */ + private List groupNonPrefixes(Node node, Function, FormatNode> groupFn) { + var children = node.children; + var index = -1; + for (var i = 0; i < children.size(); i++) { + var child = children.get(i); + if (!child.type.isAffix() + && child.type != NodeType.DOC_COMMENT + && child.type != NodeType.ANNOTATION) { + index = i; + break; + } + } + if (index <= 0) { + // no prefixes + return List.of(groupFn.apply(children)); + } + var prefixes = children.subList(0, index); + var rest = children.subList(index, children.size()); + var res = new ArrayList<>(formatGeneric(prefixes, spaceOrLine())); + res.add(getSeparator(prefixes.get(prefixes.size() - 1), rest.get(0), spaceOrLine())); + res.add(groupFn.apply(rest)); + return res; + } + + private String getImportUrl(Node node) { + var strChars = node.findChildByType(NodeType.STRING_CHARS); + assert strChars != null; + var txt = strChars.text(source); + return txt.substring(1, txt.length() - 1); + } + + private FormatNode getSeparator(Node prev, Node next, FormatNode separator) { + var base = getBaseSeparator(prev, next); + return base != null ? base : separator; + } + + private FormatNode getSeparator( + Node prev, Node next, BiFunction separatorFn) { + var base = getBaseSeparator(prev, next); + return base != null ? base : separatorFn.apply(prev, next); + } + + private FormatNode getBaseSeparator(Node prev, Node next) { + if (endsInLineComment(prev)) { + return linesBetween(prev, next) > 1 ? TWO_NEWLINES : mustForceLine(); + } + if (hasTrailingAffix(prev, next)) return Space.INSTANCE; + if (prev.type == NodeType.DOC_COMMENT) return mustForceLine(); + if (prev.type == NodeType.ANNOTATION) return forceLine(); + if (FORCE_LINE_AFFIXES.contains(prev.type) || next.type.isAffix()) { + return linesBetween(prev, next) > 1 ? TWO_NEWLINES : mustForceLine(); + } + if (prev.type == NodeType.BLOCK_COMMENT) { + return linesBetween(prev, next) > 0 ? forceSpaceyLine() : Space.INSTANCE; + } + if (EMPTY_SUFFIXES.contains(next.type) + || isTerminal(prev, "[", "!", "@", "[[") + || isTerminal(next, "]", "?", ",")) { + return Empty.INSTANCE; + } + if (isTerminal(prev, "class", "function", "new") + || isTerminal(next, "=", "{", "->", "class", "function") + || next.type == NodeType.OBJECT_BODY + || prev.type == NodeType.MODIFIER_LIST) { + return Space.INSTANCE; + } + if (next.type == NodeType.DOC_COMMENT) return TWO_NEWLINES; + return null; + } + + private static boolean endsInLineComment(Node node) { + while (true) { + if (node.type == NodeType.LINE_COMMENT) return true; + if (node.children.isEmpty()) return false; + node = node.children.get(node.children.size() - 1); + } + } + + private FormatNode line() { + return noNewlines ? Empty.INSTANCE : Line.INSTANCE; + } + + private FormatNode spaceOrLine() { + return noNewlines ? Space.INSTANCE : SpaceOrLine.INSTANCE; + } + + private FormatNode mustForceLine() { + if (noNewlines) { + // should never happen; we do not set `noNewlines` for interpolation blocks that span multiple + // lines + throw new RuntimeException("Tried to render Pkl code as single line"); + } + return ForceLine.INSTANCE; + } + + private FormatNode forceLine() { + return noNewlines ? Empty.INSTANCE : ForceLine.INSTANCE; + } + + private FormatNode forceSpaceyLine() { + return noNewlines ? Space.INSTANCE : ForceLine.INSTANCE; + } + + private FormatNode ifWrap(int id, FormatNode ifWrap, FormatNode ifNotWrap) { + return noNewlines ? ifNotWrap : new IfWrap(id, ifWrap, ifNotWrap); + } + + private static boolean hasTrailingAffix(Node node, Node next) { + var n = next; + while (n != null) { + if (n.type.isAffix() && node.span.lineEnd() == n.span.lineBegin()) return true; + n = n.children.isEmpty() ? null : n.children.get(0); + } + return false; + } + + private int modifierPrecedence(Node modifier) { + var txt = modifier.text(source); + return switch (txt) { + case "abstract", "open" -> 0; + case "external" -> 1; + case "local", "hidden" -> 2; + case "fixed", "const" -> 3; + default -> throw new RuntimeException("Unknown modifier `" + txt + "`"); + }; + } + + private static boolean isSameLineExpr(Node node) { + return SAME_LINE_EXPRS.contains(node.type); + } + + @SuppressWarnings("unchecked") + private static List[] splitPrefixes(List nodes) { + var splitPoint = 0; + for (var i = 0; i < nodes.size(); i++) { + if (!nodes.get(i).type.isAffix() && nodes.get(i).type != NodeType.DOC_COMMENT) { + splitPoint = i; + break; + } + } + return new List[] {nodes.subList(0, splitPoint), nodes.subList(splitPoint, nodes.size())}; + } + + private List indentAfterFirstNewline(List nodes, boolean group) { + var index = -1; + for (var i = 0; i < nodes.size(); i++) { + var n = nodes.get(i); + if (n instanceof SpaceOrLine || n instanceof ForceLine || n instanceof Line) { + index = i; + break; + } + } + if (index <= 0) return nodes; + FormatNode indented; + if (group) { + indented = group(new Indent(nodes.subList(index, nodes.size()))); + } else { + indented = new Indent(nodes.subList(index, nodes.size())); + } + var result = new ArrayList<>(nodes.subList(0, index)); + result.add(indented); + return result; + } + + private FormatNode groupOnSpace(List fnodes) { + var res = new ArrayList(); + for (var i = 0; i < fnodes.size(); i++) { + var node = fnodes.get(i); + if (i > 0 && (node instanceof SpaceOrLine || node instanceof Space)) { + res.add(groupOnSpace(fnodes.subList(i, fnodes.size()))); + break; + } else { + res.add(node); + } + } + return new Group(newId(), res); + } + + /** Flatten binary operators by precedence */ + private List flattenBinaryOperatorExprs(Node node) { + Node opNode = null; + for (var child : node.children) { + if (child.type == NodeType.OPERATOR) { + opNode = child; + break; + } + } + assert opNode != null; + var op = text(opNode); + return flattenBinaryOperatorExprs(node, Operator.byName(op).getPrec()); + } + + private List flattenBinaryOperatorExprs(Node node, int prec) { + Node opNode = null; + for (var child : node.children) { + if (child.type == NodeType.OPERATOR) { + opNode = child; + break; + } + } + assert opNode != null; + if (prec != Operator.byName(text(opNode)).getPrec()) return List.of(node); + var result = new ArrayList(); + for (var child : node.children) { + if (child.type == NodeType.BINARY_OP_EXPR) { + result.addAll(flattenBinaryOperatorExprs(child, prec)); + } else { + result.add(child); + } + } + return result; + } + + private static int linesBetween(Node prev, Node next) { + return next.span.lineBegin() - prev.span.lineEnd(); + } + + private String text(Node node) { + return node.text(source); + } + + private boolean isTerminal(Node node, String... texts) { + if (node.type != NodeType.TERMINAL) return false; + var t = node.text(source); + for (var text : texts) { + if (t.equals(text)) return true; + } + return false; + } + + private boolean isTerminalSingle(Node node, String text) { + return node.type == NodeType.TERMINAL && node.text(source).equals(text); + } + + private int newId() { + return id++; + } + + private static Nodes nodes(FormatNode... nodes) { + return new Nodes(List.of(nodes)); + } + + private Group group(FormatNode... nodes) { + return new Group(newId(), List.of(nodes)); + } + + private static Indent indent(FormatNode... nodes) { + return new Indent(List.of(nodes)); + } + + private static Node firstProperChild(Node node) { + for (var child : node.children) { + if (isProper(child)) return child; + } + return null; + } + + private static Node lastProperNode(List nodes) { + for (var i = nodes.size() - 1; i >= 0; i--) { + if (isProper(nodes.get(i))) return nodes.get(i); + } + return null; + } + + // returns true if this node is not an affix or terminal + private static boolean isProper(Node node) { + return !node.type.isAffix() && node.type != NodeType.TERMINAL; + } + + private static boolean isMultiline(Node node) { + return node.span.lineBegin() < node.span.lineEnd(); + } + + private static boolean isMultilineList(List nodes) { + if (nodes.isEmpty()) return false; + return nodes.get(0).span.lineBegin() < nodes.get(nodes.size() - 1).span.lineEnd(); + } + + @SuppressWarnings("unchecked") + private static List[] splitOn(List list, Predicate pred) { + var index = -1; + for (var i = 0; i < list.size(); i++) { + if (pred.test(list.get(i))) { + index = i; + break; + } + } + if (index == -1) { + return new List[] {list, List.of()}; + } + return new List[] {list.subList(0, index), list.subList(index, list.size())}; + } + + @SafeVarargs + private static List concat(List... lists) { + var result = new ArrayList(); + for (var list : lists) { + result.addAll(list); + } + return result; + } + + private static List concat(List a, T elem, List b) { + var result = new ArrayList(a.size() + 1 + b.size()); + result.addAll(a); + result.add(elem); + result.addAll(b); + return result; + } + + static final class PeekableIterator implements Iterator { + private final Iterator iterator; + private T peeked; + private boolean hasPeeked = false; + + PeekableIterator(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public T next() { + if (hasPeeked) { + hasPeeked = false; + return peeked; + } + return iterator.next(); + } + + @Override + public boolean hasNext() { + return hasPeeked || iterator.hasNext(); + } + + T peek() { + if (!hasNext()) throw new NoSuchElementException(); + if (hasPeeked) return peeked; + peeked = iterator.next(); + hasPeeked = true; + return peeked; + } + + List takeUntilBefore(Predicate predicate) { + var result = new ArrayList(); + while (true) { + if (!hasNext() || predicate.test(peek())) { + return result; + } + result.add(next()); + } + } + } + + private static final Pattern ABSOLUTE_URL_REGEX = Pattern.compile("\\w+:.*"); + + private static final Nodes TWO_NEWLINES = + new Nodes(List.of(ForceLine.INSTANCE, ForceLine.INSTANCE)); + + private static final EnumSet FORCE_LINE_AFFIXES = + EnumSet.of( + NodeType.DOC_COMMENT_LINE, NodeType.LINE_COMMENT, NodeType.SEMICOLON, NodeType.SHEBANG); + + private static final EnumSet EMPTY_SUFFIXES = + EnumSet.of( + NodeType.TYPE_ARGUMENT_LIST, + NodeType.TYPE_ANNOTATION, + NodeType.TYPE_PARAMETER_LIST, + NodeType.PARAMETER_LIST); + + private static final EnumSet SAME_LINE_EXPRS = + EnumSet.of(NodeType.NEW_EXPR, NodeType.AMENDS_EXPR, NodeType.FUNCTION_LITERAL_EXPR); + + private static final class ImportComparator implements java.util.Comparator { + private final char[] source; + + ImportComparator(char[] source) { + this.source = source; + } + + @Override + public int compare(Node o1, Node o2) { + var import1 = o1.findChildByType(NodeType.STRING_CHARS); + var import2 = o2.findChildByType(NodeType.STRING_CHARS); + if (import1 == null || import2 == null) { + // should never happen + throw new RuntimeException("ImportComparator: not an import"); + } + return new NaturalOrderComparator(true).compare(import1.text(source), import2.text(source)); + } + } +} diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/FormatNode.java b/pkl-formatter/src/main/java/org/pkl/formatter/FormatNode.java new file mode 100644 index 00000000..c5ee2f56 --- /dev/null +++ b/pkl-formatter/src/main/java/org/pkl/formatter/FormatNode.java @@ -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 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 nodes) implements FormatNode {} + +record Nodes(List nodes) implements FormatNode {} + +record Group(int id, List nodes) implements FormatNode {} + +record MultilineStringGroup(int endQuoteCol, List nodes) implements FormatNode {} + +record IfWrap(int id, FormatNode ifWrap, FormatNode ifNotWrap) implements FormatNode {} diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java b/pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java new file mode 100644 index 00000000..6e9487a7 --- /dev/null +++ b/pkl-formatter/src/main/java/org/pkl/formatter/Formatter.java @@ -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. + * + *

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); + } +} diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/Generator.java b/pkl-formatter/src/main/java/org/pkl/formatter/Generator.java new file mode 100644 index 00000000..85f9d232 --- /dev/null +++ b/pkl-formatter/src/main/java/org/pkl/formatter/Generator.java @@ -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 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; + } +} diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/GrammarVersion.java b/pkl-formatter/src/main/java/org/pkl/formatter/GrammarVersion.java new file mode 100644 index 00000000..7b744d37 --- /dev/null +++ b/pkl-formatter/src/main/java/org/pkl/formatter/GrammarVersion.java @@ -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; + } +} diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/NaturalOrderComparator.java b/pkl-formatter/src/main/java/org/pkl/formatter/NaturalOrderComparator.java new file mode 100644 index 00000000..ef522acf --- /dev/null +++ b/pkl-formatter/src/main/java/org/pkl/formatter/NaturalOrderComparator.java @@ -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 { + + 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) {} +} diff --git a/pkl-formatter/src/main/java/org/pkl/formatter/Wrap.java b/pkl-formatter/src/main/java/org/pkl/formatter/Wrap.java new file mode 100644 index 00000000..1fe73740 --- /dev/null +++ b/pkl-formatter/src/main/java/org/pkl/formatter/Wrap.java @@ -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; + } +} diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt deleted file mode 100644 index bbcb93a6..00000000 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt +++ /dev/null @@ -1,1649 +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.util.EnumSet -import kotlin.collections.withIndex -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.parser.syntax.Operator -import org.pkl.parser.syntax.generic.Node -import org.pkl.parser.syntax.generic.NodeType - -internal class Builder(sourceText: String, private val grammarVersion: GrammarVersion) { - private var id: Int = 0 - private val source: CharArray = sourceText.toCharArray() - private var noNewlines = false - - fun format(node: Node): FormatNode = - when (node.type) { - NodeType.MODULE -> formatModule(node) - NodeType.DOC_COMMENT -> Nodes(formatGeneric(node.children, null)) - NodeType.DOC_COMMENT_LINE -> formatDocComment(node) - NodeType.LINE_COMMENT, - NodeType.BLOCK_COMMENT, - NodeType.TERMINAL, - NodeType.MODIFIER, - NodeType.IDENTIFIER, - NodeType.STRING_CHARS, - NodeType.STRING_ESCAPE, - NodeType.INT_LITERAL_EXPR, - NodeType.FLOAT_LITERAL_EXPR, - NodeType.BOOL_LITERAL_EXPR, - NodeType.THIS_EXPR, - NodeType.OUTER_EXPR, - NodeType.MODULE_EXPR, - NodeType.NULL_EXPR, - NodeType.MODULE_TYPE, - NodeType.UNKNOWN_TYPE, - NodeType.NOTHING_TYPE, - NodeType.SHEBANG, - NodeType.OPERATOR -> Text(node.text(source)) - NodeType.STRING_NEWLINE -> mustForceLine() - NodeType.MODULE_DECLARATION -> formatModuleDeclaration(node) - NodeType.MODULE_DEFINITION -> formatModuleDefinition(node) - NodeType.SINGLE_LINE_STRING_LITERAL_EXPR -> formatSingleLineString(node) - NodeType.MULTI_LINE_STRING_LITERAL_EXPR -> formatMultilineString(node) - NodeType.ANNOTATION -> formatAnnotation(node) - NodeType.TYPEALIAS -> formatTypealias(node) - NodeType.TYPEALIAS_HEADER -> formatTypealiasHeader(node) - NodeType.TYPEALIAS_BODY -> formatTypealiasBody(node) - NodeType.MODIFIER_LIST -> formatModifierList(node) - NodeType.PARAMETER_LIST -> formatParameterList(node) - NodeType.PARAMETER_LIST_ELEMENTS -> formatParameterListElements(node) - NodeType.TYPE_PARAMETER_LIST -> formatTypeParameterList(node) - NodeType.TYPE_PARAMETER_LIST_ELEMENTS -> formatParameterListElements(node) - NodeType.TYPE_PARAMETER -> Group(newId(), formatGeneric(node.children, spaceOrLine())) - NodeType.PARAMETER -> formatParameter(node) - NodeType.EXTENDS_CLAUSE, - NodeType.AMENDS_CLAUSE -> formatAmendsExtendsClause(node) - NodeType.IMPORT_LIST -> formatImportList(node) - NodeType.IMPORT -> formatImport(node) - NodeType.IMPORT_ALIAS -> Group(newId(), formatGeneric(node.children, spaceOrLine())) - NodeType.CLASS -> formatClass(node) - NodeType.CLASS_HEADER -> formatClassHeader(node) - NodeType.CLASS_HEADER_EXTENDS -> formatClassHeaderExtends(node) - NodeType.CLASS_BODY -> formatClassBody(node) - NodeType.CLASS_BODY_ELEMENTS -> formatClassBodyElements(node) - NodeType.CLASS_PROPERTY, - NodeType.OBJECT_PROPERTY, - NodeType.OBJECT_ENTRY -> formatClassProperty(node) - NodeType.CLASS_PROPERTY_HEADER, - NodeType.OBJECT_PROPERTY_HEADER -> formatClassPropertyHeader(node) - NodeType.CLASS_PROPERTY_HEADER_BEGIN, - NodeType.OBJECT_PROPERTY_HEADER_BEGIN -> formatClassPropertyHeaderBegin(node) - NodeType.CLASS_PROPERTY_BODY, - NodeType.OBJECT_PROPERTY_BODY -> formatClassPropertyBody(node) - NodeType.CLASS_METHOD, - NodeType.OBJECT_METHOD -> formatClassMethod(node) - NodeType.CLASS_METHOD_HEADER -> formatClassMethodHeader(node) - NodeType.CLASS_METHOD_BODY -> formatClassMethodBody(node) - NodeType.OBJECT_BODY -> formatObjectBody(node) - NodeType.OBJECT_ELEMENT -> format(node.children[0]) // has a single element - NodeType.OBJECT_ENTRY_HEADER -> formatObjectEntryHeader(node) - NodeType.FOR_GENERATOR -> formatForGenerator(node) - NodeType.FOR_GENERATOR_HEADER -> formatForGeneratorHeader(node) - NodeType.FOR_GENERATOR_HEADER_DEFINITION -> formatForGeneratorHeaderDefinition(node) - NodeType.FOR_GENERATOR_HEADER_DEFINITION_HEADER -> - formatForGeneratorHeaderDefinitionHeader(node) - NodeType.WHEN_GENERATOR -> formatWhenGenerator(node) - NodeType.WHEN_GENERATOR_HEADER -> formatWhenGeneratorHeader(node) - NodeType.OBJECT_SPREAD -> Nodes(formatGeneric(node.children, null)) - NodeType.MEMBER_PREDICATE -> formatMemberPredicate(node) - NodeType.QUALIFIED_IDENTIFIER -> formatQualifiedIdentifier(node) - NodeType.ARGUMENT_LIST -> formatArgumentList(node) - NodeType.ARGUMENT_LIST_ELEMENTS -> formatArgumentListElements(node) - NodeType.OBJECT_PARAMETER_LIST -> formatObjectParameterList(node) - NodeType.IF_EXPR -> formatIf(node) - NodeType.IF_HEADER -> formatIfHeader(node) - NodeType.IF_CONDITION -> formatIfCondition(node) - NodeType.IF_CONDITION_EXPR -> Indent(formatGeneric(node.children, null)) - NodeType.IF_THEN_EXPR -> formatIfThen(node) - NodeType.IF_ELSE_EXPR -> formatIfElse(node) - NodeType.NEW_EXPR, - NodeType.AMENDS_EXPR -> formatNewExpr(node) - NodeType.NEW_HEADER -> formatNewHeader(node) - NodeType.UNQUALIFIED_ACCESS_EXPR -> formatUnqualifiedAccessExpression(node) - NodeType.QUALIFIED_ACCESS_EXPR -> formatQualifiedAccessExpression(node) - NodeType.BINARY_OP_EXPR -> formatBinaryOpExpr(node) - NodeType.FUNCTION_LITERAL_EXPR -> formatFunctionLiteralExpr(node) - NodeType.FUNCTION_LITERAL_BODY -> formatFunctionLiteralBody(node) - NodeType.SUBSCRIPT_EXPR, - NodeType.SUPER_SUBSCRIPT_EXPR -> formatSubscriptExpr(node) - NodeType.TRACE_EXPR -> formatTraceThrowReadExpr(node) - NodeType.THROW_EXPR -> formatTraceThrowReadExpr(node) - NodeType.READ_EXPR -> formatTraceThrowReadExpr(node) - NodeType.NON_NULL_EXPR -> Nodes(formatGeneric(node.children, null)) - NodeType.SUPER_ACCESS_EXPR -> Nodes(formatGeneric(node.children, null)) - NodeType.PARENTHESIZED_EXPR -> formatParenthesizedExpr(node) - NodeType.PARENTHESIZED_EXPR_ELEMENTS -> formatParenthesizedExprElements(node) - NodeType.IMPORT_EXPR -> Nodes(formatGeneric(node.children, null)) - NodeType.LET_EXPR -> formatLetExpr(node) - NodeType.LET_PARAMETER_DEFINITION -> formatLetParameterDefinition(node) - NodeType.LET_PARAMETER -> formatLetParameter(node) - NodeType.UNARY_MINUS_EXPR -> Nodes(formatGeneric(node.children, null)) - NodeType.LOGICAL_NOT_EXPR -> Nodes(formatGeneric(node.children, null)) - NodeType.TYPE_ANNOTATION -> formatTypeAnnotation(node) - NodeType.TYPE_ARGUMENT_LIST -> formatTypeParameterList(node) - NodeType.TYPE_ARGUMENT_LIST_ELEMENTS -> formatParameterListElements(node) - NodeType.DECLARED_TYPE -> formatDeclaredType(node) - NodeType.CONSTRAINED_TYPE -> formatConstrainedType(node) - NodeType.CONSTRAINED_TYPE_CONSTRAINT -> formatParameterList(node) - NodeType.CONSTRAINED_TYPE_ELEMENTS -> formatParameterListElements(node) - NodeType.NULLABLE_TYPE -> Nodes(formatGeneric(node.children, null)) - NodeType.UNION_TYPE -> formatUnionType(node) - NodeType.FUNCTION_TYPE -> formatFunctionType(node) - NodeType.FUNCTION_TYPE_PARAMETERS -> formatParameterList(node) - NodeType.STRING_CONSTANT_TYPE -> format(node.children[0]) - NodeType.PARENTHESIZED_TYPE -> formatParenthesizedType(node) - NodeType.PARENTHESIZED_TYPE_ELEMENTS -> formatParenthesizedTypeElements(node) - else -> throw RuntimeException("Unknown node type: ${node.type}") - } - - private fun formatModule(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - if (prev.linesBetween(next) > 1) TWO_NEWLINES else forceLine() - } - return Nodes(nodes) - } - - private fun formatModuleDeclaration(node: Node): FormatNode { - return Nodes(formatGeneric(node.children, TWO_NEWLINES)) - } - - private fun formatModuleDefinition(node: Node): FormatNode { - val (prefixes, nodes) = splitPrefixes(node.children) - val fnodes = - formatGenericWithGen(nodes, Space) { node, _ -> - if (node.type == NodeType.QUALIFIED_IDENTIFIER) { - Nodes(formatGeneric(node.children, null)) - } else { - format(node) - } - } - val res = Nodes(fnodes) - return if (prefixes.isEmpty()) { - res - } else { - val sep = getSeparator(prefixes.last(), nodes.first(), spaceOrLine()) - Nodes(formatGeneric(prefixes, spaceOrLine()) + listOf(sep, res)) - } - } - - private fun formatDocComment(node: Node): FormatNode { - val txt = node.text() - if (txt == "///" || txt == "/// ") return Text("///") - - var comment = txt.substring(3) - if (comment.isStrictBlank()) return Text("///") - - if (comment.isNotEmpty() && comment[0] != ' ') comment = " $comment" - return Text("///$comment") - } - - private fun String.isStrictBlank(): Boolean { - for (ch in this) { - if (ch != ' ' && ch != '\t') return false - } - return true - } - - private fun formatQualifiedIdentifier(node: Node): FormatNode { - // short circuit - if (node.children.size == 1) return format(node.children[0]) - - val first = listOf(format(node.children[0]), line()) - val nodes = - formatGeneric(node.children.drop(1)) { n1, _ -> - if (n1.type == NodeType.TERMINAL) null else line() - } - return Group(newId(), first + listOf(Indent(nodes))) - } - - private fun formatUnqualifiedAccessExpression(node: Node): FormatNode { - val children = node.children - if (children.size == 1) return format(children[0]) - val firstNode = node.firstProperChild()!! - return if (firstNode.text() == "Map") { - val nodes = - formatGenericWithGen(children, null) { node, _ -> - if (node.type == NodeType.ARGUMENT_LIST) { - formatArgumentList(node, twoBy2 = true) - } else { - format(node) - } - } - Nodes(nodes) - } else { - Nodes(formatGeneric(children, null)) - } - } - - /** - * Special cases when formatting qualified access: - * - * Case 1: Dot calls followed by closing method call: wrap after the opening paren. - * - * ``` - * foo.bar.baz(new { - * qux = 1 - * }) - * ``` - * - * Case 2: Dot calls, then method calls: group the leading access together. - * - * ``` - * foo.bar - * .baz(new { - * qux = 1 - * }) - * .baz() - * ``` - * - * Case 3: If there are multiple lambdas present, always force a newline. - * - * ``` - * foo - * .map((it) -> it + 1) - * .filter((it) -> it.isEven) - * ``` - */ - private fun formatQualifiedAccessExpression(node: Node): FormatNode { - var lambdaCount = 0 - var methodCallCount = 0 - var indexBeforeFirstMethodCall = 0 - val flat = mutableListOf() - - fun gatherFacts(current: Node) { - for (child in current.children) { - if (child.type == NodeType.QUALIFIED_ACCESS_EXPR) { - gatherFacts(child) - } else { - flat.add(child) - when { - isMethodCall(child) -> { - methodCallCount++ - if (hasFunctionLiteral(child, 2)) { - lambdaCount++ - } - } - methodCallCount == 0 -> { - indexBeforeFirstMethodCall = flat.lastIndex - } - } - } - } - } - - gatherFacts(node) - - val leadingSeparator: (Node, Node) -> FormatNode? = { prev, next -> - when { - prev.type == NodeType.OPERATOR -> null - next.type == NodeType.OPERATOR -> line() - else -> spaceOrLine() - } - } - - val trailingSeparator: (Node, Node) -> FormatNode? = { prev, next -> - when { - prev.type == NodeType.OPERATOR -> null - next.type == NodeType.OPERATOR -> if (lambdaCount > 1) forceLine() else line() - else -> spaceOrLine() - } - } - - val nodes = - when { - methodCallCount == 1 && isMethodCall(flat.lastProperNode()!!) -> { - // lift argument list into its own node - val (callChain, argsList) = splitFunctionCallNode(flat) - val leadingNodes = - indentAfterFirstNewline(formatGeneric(callChain, leadingSeparator), true) - val trailingNodes = formatGeneric(argsList, trailingSeparator) - val sep = getBaseSeparator(callChain.last(), argsList.first()) - if (sep != null) { - leadingNodes + sep + trailingNodes - } else { - leadingNodes + trailingNodes - } - } - methodCallCount > 0 && indexBeforeFirstMethodCall > 0 -> { - val leading = flat.subList(0, indexBeforeFirstMethodCall) - val trailing = flat.subList(indexBeforeFirstMethodCall, flat.size) - val leadingNodes = indentAfterFirstNewline(formatGeneric(leading, leadingSeparator), true) - val trailingNodes = formatGeneric(trailing, trailingSeparator) - leadingNodes + line() + trailingNodes - } - else -> formatGeneric(flat, trailingSeparator) - } - - val shouldGroup = node.children.size == flat.size - return Group(newId(), indentAfterFirstNewline(nodes, shouldGroup)) - } - - /** - * Split a function call node to extract its identifier into the leading group. For example, - * `foo.bar(5)` becomes: leading gets `foo.bar`, rest gets `(5)`. - */ - private fun splitFunctionCallNode(nodes: List): Pair, List> { - assert(nodes.isNotEmpty()) - val lastNode = nodes.last() - val argListIdx = lastNode.children.indexOfFirst { it.type == NodeType.ARGUMENT_LIST } - val leading = nodes.subList(0, nodes.lastIndex) + lastNode.children.subList(0, argListIdx) - val trailing = lastNode.children.subList(argListIdx, lastNode.children.size) - return leading to trailing - } - - private fun isMethodCall(node: Node): Boolean { - if (node.type != NodeType.UNQUALIFIED_ACCESS_EXPR) return false - for (child in node.children) { - if (child.type == NodeType.ARGUMENT_LIST) { - return true - } - } - return false - } - - private fun formatAmendsExtendsClause(node: Node): FormatNode { - val prefix = formatGeneric(node.children.dropLast(1), spaceOrLine()) - // string constant - val suffix = Indent(listOf(format(node.children.last()))) - return Group(newId(), prefix + listOf(spaceOrLine()) + suffix) - } - - private fun formatImport(node: Node): FormatNode { - return Group( - newId(), - formatGenericWithGen(node.children, spaceOrLine()) { node, _ -> - if (node.isTerminal("import")) format(node) else indent(format(node)) - }, - ) - } - - private fun formatAnnotation(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, spaceOrLine())) - } - - private fun formatTypealias(node: Node): FormatNode { - val nodes = - groupNonPrefixes(node) { children -> Group(newId(), formatGeneric(children, spaceOrLine())) } - return Nodes(nodes) - } - - private fun formatTypealiasHeader(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, Space)) - } - - private fun formatTypealiasBody(node: Node): FormatNode { - return Indent(formatGeneric(node.children, spaceOrLine())) - } - - private fun formatClass(node: Node): FormatNode { - return Nodes(formatGeneric(node.children, spaceOrLine())) - } - - private fun formatClassHeader(node: Node): FormatNode { - return groupOnSpace(formatGeneric(node.children, spaceOrLine())) - } - - private fun formatClassHeaderExtends(node: Node): FormatNode { - return indent(Group(newId(), formatGeneric(node.children, spaceOrLine()))) - } - - private fun formatClassBody(node: Node): FormatNode { - val children = node.children - if (children.size == 2) { - // no members - return Nodes(formatGeneric(children, null)) - } - return Group(newId(), formatGeneric(children, forceLine())) - } - - private fun formatClassBodyElements(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - val lineDiff = prev.linesBetween(next) - if (lineDiff > 1 || lineDiff == 0) TWO_NEWLINES else forceLine() - } - return Indent(nodes) - } - - private fun formatClassProperty(node: Node): FormatNode { - val sameLine = - node.children - .lastOrNull { it.isExpressionOrPropertyBody() } - ?.let { - if (it.type.isExpression) isSameLineExpr(it) else isSameLineExpr(it.children.last()) - } ?: false - val nodes = - groupNonPrefixes(node) { children -> - val nodes = - formatGenericWithGen(children, { _, _ -> if (sameLine) Space else spaceOrLine() }) { - node, - _ -> - if ((node.isExpressionOrPropertyBody()) && !sameLine) { - indent(format(node)) - } else format(node) - } - groupOnSpace(nodes) - } - return Nodes(nodes) - } - - private fun Node.isExpressionOrPropertyBody(): Boolean = - type.isExpression || - type == NodeType.CLASS_PROPERTY_BODY || - type == NodeType.OBJECT_PROPERTY_BODY - - private fun formatClassPropertyHeader(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, spaceOrLine())) - } - - private fun formatClassPropertyHeaderBegin(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, spaceOrLine())) - } - - private fun formatClassPropertyBody(node: Node): FormatNode { - return Nodes(formatGeneric(node.children, null)) - } - - private fun formatClassMethod(node: Node): FormatNode { - val prefixes = mutableListOf() - val nodes = - if (node.children[0].type == NodeType.CLASS_METHOD_HEADER) node.children - else { - val idx = node.children.indexOfFirst { it.type == NodeType.CLASS_METHOD_HEADER } - val prefixNodes = node.children.subList(0, idx) - prefixes += formatGeneric(prefixNodes, null) - prefixes += getSeparator(prefixNodes.last(), node.children[idx], forceLine()) - node.children.subList(idx, node.children.size) - } - - // Separate header (before =) and body (= and after) - val bodyIdx = nodes.indexOfFirst { it.type == NodeType.CLASS_METHOD_BODY } - 1 - val header = if (bodyIdx < 0) nodes else nodes.subList(0, bodyIdx) - val headerGroupId = newId() - val methodGroupId = newId() - val headerNodes = - formatGenericWithGen(header, spaceOrLine()) { node, _ -> - if (node.type == NodeType.PARAMETER_LIST) { - formatParameterList(node, id = headerGroupId) - } else { - format(node) - } - } - if (bodyIdx < 0) { - // body is empty, return header - return if (prefixes.isEmpty()) { - Group(headerGroupId, headerNodes) - } else { - Nodes(prefixes + Group(headerGroupId, headerNodes)) - } - } - - val bodyNodes = nodes.subList(bodyIdx, nodes.size) - - val expr = bodyNodes.last().children[0] - val isSameLineBody = isSameLineExpr(expr) - - // Format body (= and expression) - val bodyFormat = - if (isSameLineBody) { - formatGeneric(bodyNodes, Space) - } else { - formatGenericWithGen(bodyNodes, spaceOrLine()) { node, next -> - if (next == null) indent(format(node)) else format(node) - } - } - - val headerGroup = Group(headerGroupId, headerNodes) - val bodyGroup = Group(newId(), bodyFormat) - val separator = getSeparator(header.last(), bodyNodes.first(), Space) - val allNodes = Group(methodGroupId, listOf(headerGroup, separator, bodyGroup)) - - return if (prefixes.isEmpty()) allNodes else Nodes(prefixes + allNodes) - } - - private fun formatClassMethodHeader(node: Node): FormatNode { - val nodes = formatGeneric(node.children, Space) - return Nodes(nodes) - } - - private fun formatClassMethodBody(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, null)) - } - - private fun formatParameter(node: Node): FormatNode { - if (node.children.size == 1) return format(node.children[0]) // underscore - return Group(newId(), formatGeneric(node.children, spaceOrLine())) - } - - private fun formatParameterList(node: Node, id: Int? = null): FormatNode { - if (node.children.size == 2) return Text("()") - val groupId = id ?: newId() - val nodes = - formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) { - if (next.isTerminal(")")) { - // trailing comma - if (grammarVersion == GrammarVersion.V1) { - line() - } else { - ifWrap(groupId, nodes(Text(","), line()), line()) - } - } else line() - } else spaceOrLine() - } - return if (id != null) Nodes(nodes) else Group(groupId, nodes) - } - - private fun formatArgumentList(node: Node, twoBy2: Boolean = false): FormatNode { - if (node.children.size == 2) return Text("()") - val hasTrailingLambda = hasTrailingLambda(node) - val groupId = newId() - val nodes = - formatGenericWithGen( - node.children, - { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) { - val node = if (hasTrailingLambda) Empty else line() - if (next.isTerminal(")") && !hasTrailingLambda) { - // trailing comma - if (grammarVersion == GrammarVersion.V1) { - node - } else { - ifWrap(groupId, nodes(Text(","), node), node) - } - } else node - } else spaceOrLine() - }, - ) { node, _ -> - if (node.type == NodeType.ARGUMENT_LIST_ELEMENTS) { - formatArgumentListElements(node, hasTrailingLambda, twoBy2 = twoBy2) - } else format(node) - } - return Group(groupId, nodes) - } - - private fun formatArgumentListElements( - node: Node, - hasTrailingLambda: Boolean = false, - twoBy2: Boolean = false, - ): FormatNode { - val children = node.children - val shouldMultiline = shouldMultilineNodes(node) { it.isTerminal(",") } - val sep: (Node, Node) -> FormatNode = { _, _ -> - if (shouldMultiline) forceSpaceyLine() else spaceOrLine() - } - return if (twoBy2) { - val pairs = pairArguments(children) - val nodes = - formatGenericWithGen(pairs, sep) { node, _ -> - if (node.type == NodeType.ARGUMENT_LIST_ELEMENTS) { - Group(newId(), formatGeneric(node.children, spaceOrLine())) - } else { - format(node) - } - } - Indent(nodes) - } else if (hasTrailingLambda) { - // if the args have a trailing lambda, group them differently - val splitIndex = children.indexOfLast { it.type in SAME_LINE_EXPRS } - val normalParams = children.subList(0, splitIndex) - val lastParam = children.subList(splitIndex, children.size) - val trailingNode = if (endsWithClosingCurlyBrace(lastParam.last())) Empty else line() - val lastNodes = formatGenericWithGen(lastParam, sep, null) - if (normalParams.isEmpty()) { - val lastNodes = formatGenericWithGen(lastParam, sep, null) - group(Group(newId(), lastNodes), trailingNode) - } else { - val separator = getSeparator(normalParams.last(), lastParam[0], Space) - val paramNodes = formatGenericWithGen(normalParams, sep, null) - group(Group(newId(), paramNodes), separator, Group(newId(), lastNodes), trailingNode) - } - } else { - Indent(formatGeneric(children, sep)) - } - } - - private fun shouldMultilineNodes(node: Node, predicate: (Node) -> Boolean): Boolean { - for (idx in 0.. 0) { - return true - } - } - return false - } - - private tailrec fun endsWithClosingCurlyBrace(node: Node): Boolean { - return if (node.children.isNotEmpty()) { - endsWithClosingCurlyBrace(node.children.last()) - } else { - node.isTerminal("}") - } - } - - /** - * Tells if an argument list has a trailing lambda, new expr, or amends expr. - * - * Only considered trailing lamdba if: - * 1. There is only one lambda/new expr/amends expr in the list. E.g. avoid formatting `toMap()` - * weirdly: ``` foo.toMap( (it) -> makeSomeKey(it), (it) -> makeSomeValue(it), ) ``` - * 2. The lambda does not have leading or trailing line comment. - */ - private fun hasTrailingLambda(argList: Node): Boolean { - val children = argList.firstProperChild()?.children ?: return false - var seenLambda = false - if (children.last().type == NodeType.LINE_COMMENT) { - return false - } - for (i in children.lastIndex downTo 0) { - val child = children[i] - if (!child.isProper()) continue - if (!seenLambda) { - if (child.type !in SAME_LINE_EXPRS) { - return false - } - // preceded by line comment - if (children.getOrNull(i - 1)?.type == NodeType.LINE_COMMENT) return false - seenLambda = true - } else if (child.type in SAME_LINE_EXPRS) { - return false - } - } - return true - } - - private fun pairArguments(nodes: List): List { - val res = mutableListOf() - var tmp = mutableListOf() - var commas = 0 - for (node in nodes) { - if (node.isTerminal(",")) { - commas++ - if (commas == 2) { - val suffixes = mutableListOf() - while (tmp.isNotEmpty() && tmp.last().type.isAffix) { - // trailing comments should not be paired - suffixes += tmp.removeLast() - } - res += Node(NodeType.ARGUMENT_LIST_ELEMENTS, tmp) - while (suffixes.isNotEmpty()) { - res += suffixes.removeFirst() - } - res += node - commas = 0 - tmp = mutableListOf() - } else { - tmp += node - } - } else if (tmp.isEmpty() && node.type.isAffix) { - // leading comments should not be paired - res += node - } else { - tmp += node - } - } - if (tmp.isNotEmpty()) { - res += Node(NodeType.ARGUMENT_LIST_ELEMENTS, tmp) - } - return res - } - - private fun formatParameterListElements(node: Node): FormatNode { - return Indent(formatGeneric(node.children, spaceOrLine())) - } - - private fun formatTypeParameterList(node: Node): FormatNode { - if (node.children.size == 2) return Text("<>") - val id = newId() - val nodes = - formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("<") || next.isTerminal(">")) { - if (next.isTerminal(">")) { - // trailing comma - if (grammarVersion == GrammarVersion.V1) { - Line - } else { - ifWrap(id, nodes(Text(","), line()), line()) - } - } else line() - } else spaceOrLine() - } - return Group(id, nodes) - } - - private fun formatObjectParameterList(node: Node): FormatNode { - // object param lists don't have trailing commas, as they have a trailing -> - val groupId = newId() - val nonWrappingNodes = Nodes(formatGeneric(node.children, spaceOrLine())) - // double indent the params if they wrap - val wrappingNodes = indent(Indent(listOf(line()) + nonWrappingNodes)) - return Group(groupId, listOf(ifWrap(groupId, wrappingNodes, nodes(Space, nonWrappingNodes)))) - } - - private fun formatObjectBody(node: Node): FormatNode { - if (node.children.size == 2) return Text("{}") - val groupId = newId() - val nodes = - formatGenericWithGen( - node.children, - { prev, next -> - if (next.type == NodeType.OBJECT_PARAMETER_LIST) Empty - else if (prev.isTerminal("{") || next.isTerminal("}")) { - val lines = prev.linesBetween(next) - if (lines == 0) spaceOrLine() else forceSpaceyLine() - } else spaceOrLine() - }, - ) { node, _ -> - if (node.type == NodeType.OBJECT_MEMBER_LIST) { - formatObjectMemberList(node, groupId) - } else format(node) - } - return Group(groupId, nodes) - } - - private fun formatObjectMemberList(node: Node, groupId: Int): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - val lines = prev.linesBetween(next) - when (lines) { - 0 -> ifWrap(groupId, line(), Text("; ")) - 1 -> forceLine() - else -> TWO_NEWLINES - } - } - return Indent(nodes) - } - - private fun formatObjectEntryHeader(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, spaceOrLine())) - } - - private fun formatForGenerator(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - if ( - prev.type == NodeType.FOR_GENERATOR_HEADER || next.type == NodeType.FOR_GENERATOR_HEADER - ) { - Space - } else spaceOrLine() - } - return Group(newId(), nodes) - } - - private fun formatForGeneratorHeader(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) line() else null - } - return Group(newId(), nodes) - } - - private fun formatForGeneratorHeaderDefinition(node: Node): FormatNode { - val nodes = - formatGenericWithGen( - node.children, - { _, next -> if (next.type in SAME_LINE_EXPRS) Space else spaceOrLine() }, - ) { node, _ -> - if (node.type.isExpression && node.type !in SAME_LINE_EXPRS) indent(format(node)) - else format(node) - } - return indent(Group(newId(), nodes)) - } - - private fun formatForGeneratorHeaderDefinitionHeader(node: Node): FormatNode { - val nodes = formatGeneric(node.children, spaceOrLine()) - return Group(newId(), nodes) - } - - private fun formatWhenGenerator(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - if ( - prev.type == NodeType.WHEN_GENERATOR_HEADER || - prev.isTerminal("when", "else") || - next.isTerminal("else") - ) { - Space - } else { - spaceOrLine() - } - } - return Group(newId(), nodes) - } - - private fun formatWhenGeneratorHeader(node: Node): FormatNode { - val nodes = - formatGenericWithGen( - node.children, - { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() - }, - ) { node, _ -> - if (!node.type.isAffix && node.type != NodeType.TERMINAL) { - indent(format(node)) - } else format(node) - } - return Group(newId(), nodes) - } - - private fun formatMemberPredicate(node: Node): FormatNode { - val nodes = - formatGenericWithGen(node.children, spaceOrLine()) { node, next -> - if (next == null && node.type != NodeType.OBJECT_BODY) { - indent(format(node)) - } else format(node) - } - return Group(newId(), nodes) - } - - private fun formatStringParts(nodes: List): List { - return buildList { - var isInStringInterpolation = false - val cursor = nodes.iterator().peekable() - var prev: Node? = null - while (cursor.hasNext()) { - if (isInStringInterpolation) { - val prevNoNewlines = noNewlines - val elems = cursor.takeUntilBefore { it.isTerminal(")") } - noNewlines = !elems.isMultiline() - getBaseSeparator(prev!!, elems.first())?.let { add(it) } - val formatted = formatGeneric(elems, null) - addAll(formatted) - getBaseSeparator(elems.last(), cursor.peek())?.let { add(it) } - noNewlines = prevNoNewlines - isInStringInterpolation = false - continue - } - val elem = cursor.next() - if (elem.type == NodeType.TERMINAL && elem.text().endsWith("(")) { - isInStringInterpolation = true - } - add(format(elem)) - prev = elem - } - } - } - - private fun formatSingleLineString(node: Node): FormatNode { - return Group(newId(), formatStringParts(node.children)) - } - - private fun formatMultilineString(node: Node): FormatNode { - val nodes = formatStringParts(node.children) - return MultilineStringGroup(node.children.last().span.colBegin, nodes) - } - - private fun formatIf(node: Node): FormatNode { - val separator = if (node.isMultiline()) forceSpaceyLine() else spaceOrLine() - val nodes = - formatGeneric(node.children) { _, next -> - // produce `else if` in the case of nested if. - // note: don't need to handle if `next.children[0]` is an affix because that can't be - // emitted as `else if` anyway. - if (next.type == NodeType.IF_ELSE_EXPR && next.children[0].type == NodeType.IF_EXPR) Space - else separator - } - return Group(newId(), nodes) - } - - private fun formatIfHeader(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { _, next -> - if (next.type == NodeType.IF_CONDITION) Space else spaceOrLine() - } - return Group(newId(), nodes) - } - - private fun formatIfCondition(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() - } - return Group(newId(), nodes) - } - - private fun formatIfThen(node: Node): FormatNode { - return Indent(formatGeneric(node.children, null)) - } - - private fun formatIfElse(node: Node): FormatNode { - val children = node.children - if (children.size == 1) { - val expr = children[0] - return if (expr.type == NodeType.IF_EXPR) { - // unpack the group - val group = formatIf(expr) as Group - Nodes(group.nodes) - } else { - indent(format(expr)) - } - } - return Indent(formatGeneric(node.children, null)) - } - - private fun formatNewExpr(node: Node): FormatNode { - val nodes = formatGeneric(node.children, spaceOrLine()) - return Group(newId(), nodes) - } - - private fun formatNewHeader(node: Node): FormatNode { - val nodes = formatGeneric(node.children, spaceOrLine()) - return Group(newId(), nodes) - } - - private fun formatParenthesizedExpr(node: Node): FormatNode { - if (node.children.size == 2) return Text("()") - val nodes = - formatGenericWithGen( - node.children, - { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() - }, - ) { node, _ -> - if (node.type.isExpression) indent(format(node)) else format(node) - } - return Group(newId(), nodes) - } - - private fun formatParenthesizedExprElements(node: Node): FormatNode { - return indent(Group(newId(), formatGeneric(node.children, null))) - } - - private fun formatFunctionLiteralExpr(node: Node): FormatNode { - val (params, rest) = node.children.splitOn { it.isTerminal("->") } - val sameLine = - node.children - .last { it.type == NodeType.FUNCTION_LITERAL_BODY } - .let { body -> - val expr = body.children.find { it.type.isExpression }!! - isSameLineExpr(expr) - } - val sep = if (sameLine) Space else spaceOrLine() - val bodySep = getSeparator(params.last(), rest.first(), sep) - - val nodes = formatGeneric(params, sep) - val restNodes = listOf(bodySep) + formatGeneric(rest, sep) - return Group(newId(), nodes + listOf(Group(newId(), restNodes))) - } - - private fun formatFunctionLiteralBody(node: Node): FormatNode { - val expr = node.children.find { it.type.isExpression }!! - val nodes = formatGeneric(node.children, null) - return if (isSameLineExpr(expr)) Group(newId(), nodes) else Indent(nodes) - } - - private fun formatLetExpr(node: Node): FormatNode { - val separator = if (node.isMultiline()) forceSpaceyLine() else spaceOrLine() - val endsWithLet = node.children.last().type == NodeType.LET_EXPR - val nodes = - formatGenericWithGen( - node.children, - { _, next -> if (next.type == NodeType.LET_PARAMETER_DEFINITION) Space else separator }, - ) { node, _ -> - when { - node.type == NodeType.LET_EXPR -> { - // unpack the lets - val group = formatLetExpr(node) as Group - Nodes(group.nodes) - } - endsWithLet -> format(node) - node.type.isExpression || node.type.isAffix -> indent(format(node)) - else -> format(node) - } - } - return Group(newId(), nodes) - } - - private fun formatLetParameterDefinition(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() - } - return Group(newId(), nodes) - } - - private fun formatLetParameter(node: Node): FormatNode { - return indent(formatClassProperty(node)) - } - - private fun formatBinaryOpExpr(node: Node): FormatNode { - val flat = flattenBinaryOperatorExprs(node) - val shouldMultiline = shouldMultilineNodes(node) { it.type == NodeType.OPERATOR } - val nodes = - formatGeneric(flat) { prev, next -> - val sep = if (shouldMultiline) forceSpaceyLine() else spaceOrLine() - if (prev.type == NodeType.OPERATOR) { - when (prev.text()) { - "-" -> sep - else -> Space - } - } else if (next.type == NodeType.OPERATOR) { - when (next.text()) { - "-" -> Space - else -> sep - } - } else sep - } - - val shouldGroup = node.children.size == flat.size - return Group(newId(), indentAfterFirstNewline(nodes, shouldGroup)) - } - - private fun hasFunctionLiteral(node: Node, depth: Int): Boolean { - if (node.type == NodeType.FUNCTION_LITERAL_EXPR) return true - for (child in node.children) { - if (child.type == NodeType.FUNCTION_LITERAL_EXPR) return true - if (depth > 0 && hasFunctionLiteral(child, depth - 1)) return true - } - return false - } - - private fun formatSubscriptExpr(node: Node): FormatNode { - return Nodes(formatGeneric(node.children, null)) - } - - private fun formatTraceThrowReadExpr(node: Node): FormatNode { - val nodes = - formatGenericWithGen( - node.children, - { prev, next -> if (prev.isTerminal("(") || next.isTerminal(")")) line() else null }, - ) { node, _ -> - if (node.type.isExpression) indent(format(node)) else format(node) - } - return Group(newId(), nodes) - } - - private fun formatDeclaredType(node: Node): FormatNode { - return Nodes(formatGeneric(node.children, spaceOrLine())) - } - - private fun formatConstrainedType(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { _, next -> - if (next.type == NodeType.CONSTRAINED_TYPE_CONSTRAINT) null else spaceOrLine() - } - return Group(newId(), nodes) - } - - private fun formatUnionType(node: Node): FormatNode { - val nodes = - formatGeneric(node.children) { prev, next -> - when { - next.isTerminal("|") -> spaceOrLine() - prev.isTerminal("|") -> Space - else -> null - } - } - return Group(newId(), indentAfterFirstNewline(nodes)) - } - - private fun formatFunctionType(node: Node): FormatNode { - val nodes = - formatGenericWithGen( - node.children, - { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() - }, - ) { node, next -> - if (next == null) indent(format(node)) else format(node) - } - return Group(newId(), nodes) - } - - private fun formatParenthesizedType(node: Node): FormatNode { - if (node.children.size == 2) return Text("()") - val groupId = newId() - val nodes = - formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() - } - return Group(groupId, nodes) - } - - private fun formatParenthesizedTypeElements(node: Node): FormatNode { - return indent(Group(newId(), formatGeneric(node.children, spaceOrLine()))) - } - - private fun formatTypeAnnotation(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, Space)) - } - - private fun formatModifierList(node: Node): FormatNode { - val nodes = mutableListOf() - val children = node.children.groupBy { it.type.isAffix } - if (children[true] != null) { - nodes += formatGeneric(children[true]!!, spaceOrLine()) - } - val modifiers = children[false]!!.sortedBy(::modifierPrecedence) - nodes += formatGeneric(modifiers, Space) - return Nodes(nodes) - } - - private data class ImportWithComments( - val leadingAffixes: List, - val import: Node, - val trailingAffixes: List, - ) - - private fun buildImportsWithComments(children: List): List { - val result = mutableListOf() - var pendingAffixes = mutableListOf() - var lastImport: Node? = null - var lastTrailing = mutableListOf() - var lastLeading = mutableListOf() - - for (child in children) { - if (child.type.isAffix) { - if (lastImport != null && lastImport.span.lineEnd == child.span.lineBegin) { - // trailing comment on the same line as the preceding import - lastTrailing.add(child) - } else { - // leading comment for the next import - // first, flush the previous import - if (lastImport != null) { - result.add(ImportWithComments(lastLeading, lastImport, lastTrailing)) - lastImport = null - lastTrailing = mutableListOf() - lastLeading = mutableListOf() - } - pendingAffixes.add(child) - } - } else { - // import node - if (lastImport != null) { - result.add(ImportWithComments(lastLeading, lastImport, lastTrailing)) - lastTrailing = mutableListOf() - } - lastLeading = pendingAffixes - pendingAffixes = mutableListOf() - lastImport = child - } - } - // flush the last import - if (lastImport != null) { - result.add(ImportWithComments(lastLeading, lastImport, lastTrailing)) - } - return result - } - - private fun formatImportList(node: Node): FormatNode { - val nodes = mutableListOf() - - val allImportsWithComments = buildImportsWithComments(node.children) - val imports = - allImportsWithComments.groupBy { it.import.findChildByType(NodeType.TERMINAL)?.text(source) } - if (imports["import"] != null) { - formatImportListHelper(imports["import"]!!, nodes) - if (imports["import*"] != null) nodes += TWO_NEWLINES - } - if (imports["import*"] != null) { - formatImportListHelper(imports["import*"]!!, nodes) - } - - return Nodes(nodes) - } - - private fun formatImportWithComments(entry: ImportWithComments, nodes: MutableList) { - if (entry.leadingAffixes.isNotEmpty()) { - nodes += formatGeneric(entry.leadingAffixes, spaceOrLine()) - nodes += forceLine() - } - nodes += format(entry.import) - for (affix in entry.trailingAffixes) { - nodes += Space - nodes += format(affix) - } - } - - private fun formatImportListHelper( - allImports: List, - nodes: MutableList, - ) { - val comparator = ImportComparator(source) - val imports = - allImports.groupBy { entry -> - val url = getImportUrl(entry.import) - when { - ABSOLUTE_URL_REGEX.matches(url) -> 0 - url.startsWith('@') -> 1 - else -> 2 - } - } - val absolute = imports[0]?.sortedWith(compareBy(comparator) { it.import }) - val projects = imports[1]?.sortedWith(compareBy(comparator) { it.import }) - val relatives = imports[2]?.sortedWith(compareBy(comparator) { it.import }) - var shouldNewline = false - - if (absolute != null) { - for ((i, imp) in absolute.withIndex()) { - if (i > 0) nodes += forceLine() - formatImportWithComments(imp, nodes) - } - if (projects != null || relatives != null) nodes += forceLine() - shouldNewline = true - } - - if (projects != null) { - if (shouldNewline) nodes += forceLine() - for ((i, imp) in projects.withIndex()) { - if (i > 0) nodes += forceLine() - formatImportWithComments(imp, nodes) - } - if (relatives != null) nodes += forceLine() - shouldNewline = true - } - - if (relatives != null) { - if (shouldNewline) nodes += forceLine() - for ((i, imp) in relatives.withIndex()) { - if (i > 0) nodes += forceLine() - formatImportWithComments(imp, nodes) - } - } - } - - private fun formatGeneric(children: List, separator: FormatNode?): List { - return formatGeneric(children) { _, _ -> separator } - } - - private fun formatGeneric( - children: List, - separatorFn: (Node, Node) -> FormatNode?, - ): List { - return formatGenericWithGen(children, separatorFn, null) - } - - private fun formatGenericWithGen( - children: List, - separator: FormatNode?, - generatorFn: ((Node, Node?) -> FormatNode)?, - ): List { - return formatGenericWithGen(children, { _, _ -> separator }, generatorFn) - } - - private fun formatGenericWithGen( - children: List, - separatorFn: (Node, Node) -> FormatNode?, - generatorFn: ((Node, Node?) -> FormatNode)?, - ): List { - // skip semicolons - val children = children.filter { !it.isSemicolon() } - // short circuit - if (children.isEmpty()) return emptyList() - if (children.size == 1) return listOf(format(children[0])) - - val nodes = mutableListOf() - var prev = children[0] - for (child in children.drop(1)) { - nodes += - if (generatorFn != null) { - generatorFn(prev, child) - } else { - format(prev) - } - val separator = getSeparator(prev, child, separatorFn) - if (separator != null) nodes += separator - prev = child - } - nodes += - if (generatorFn != null) { - generatorFn(children.last(), null) - } else { - format(children.last()) - } - return nodes - } - - private fun Node.isSemicolon(): Boolean = type.isAffix && text() == ";" - - /** Groups all non prefixes (comments, doc comments, annotations) of this node together. */ - private fun groupNonPrefixes(node: Node, groupFn: (List) -> FormatNode): List { - val children = node.children - val index = - children.indexOfFirst { - !it.type.isAffix && it.type != NodeType.DOC_COMMENT && it.type != NodeType.ANNOTATION - } - if (index <= 0) { - // no prefixes - return listOf(groupFn(children)) - } - val prefixes = children.subList(0, index) - val nodes = children.subList(index, children.size) - val res = mutableListOf() - res += formatGeneric(prefixes, spaceOrLine()) - res += getSeparator(prefixes.last(), nodes.first(), spaceOrLine()) - res += groupFn(nodes) - return res - } - - private fun getImportUrl(node: Node): String = - node.findChildByType(NodeType.STRING_CHARS)!!.text().drop(1).dropLast(1) - - private fun getSeparator(prev: Node, next: Node, separator: FormatNode): FormatNode { - return getBaseSeparator(prev, next) ?: separator - } - - private fun getSeparator( - prev: Node, - next: Node, - separatorFn: (Node, Node) -> FormatNode?, - ): FormatNode? { - return getBaseSeparator(prev, next) ?: separatorFn(prev, next) - } - - private fun getBaseSeparator(prev: Node, next: Node): FormatNode? { - return when { - endsInLineComment(prev) -> { - if (prev.linesBetween(next) > 1) { - TWO_NEWLINES - } else { - mustForceLine() - } - } - - hasTrailingAffix(prev, next) -> Space - prev.type == NodeType.DOC_COMMENT -> mustForceLine() - prev.type == NodeType.ANNOTATION -> forceLine() - prev.type in FORCE_LINE_AFFIXES || next.type.isAffix -> { - if (prev.linesBetween(next) > 1) { - TWO_NEWLINES - } else { - mustForceLine() - } - } - - prev.type == NodeType.BLOCK_COMMENT -> - if (prev.linesBetween(next) > 0) forceSpaceyLine() else Space - - next.type in EMPTY_SUFFIXES || - prev.isTerminal("[", "!", "@", "[[") || - next.isTerminal("]", "?", ",") -> Empty - - prev.isTerminal("class", "function", "new") || - next.isTerminal("=", "{", "->", "class", "function") || - next.type == NodeType.OBJECT_BODY || - prev.type == NodeType.MODIFIER_LIST -> Space - - next.type == NodeType.DOC_COMMENT -> TWO_NEWLINES - else -> null - } - } - - private tailrec fun endsInLineComment(node: Node): Boolean { - return when { - node.type == NodeType.LINE_COMMENT -> true - node.children.isEmpty() -> false - else -> endsInLineComment(node.children.last()) - } - } - - private fun line(): FormatNode { - return if (noNewlines) Empty else Line - } - - private fun spaceOrLine(): FormatNode { - return if (noNewlines) Space else SpaceOrLine - } - - private fun mustForceLine(): FormatNode { - if (noNewlines) { - // should never happen; we do not set `noNewlines` for interpolation blocks that span multiple - // lines - throw RuntimeException("Tried to render Pkl code as single line") - } - return ForceLine - } - - private fun forceLine(): FormatNode { - return if (noNewlines) Empty else ForceLine - } - - private fun forceSpaceyLine(): FormatNode { - return if (noNewlines) Space else ForceLine - } - - private fun ifWrap(id: Int, ifWrap: FormatNode, ifNotWrap: FormatNode): FormatNode { - return if (noNewlines) ifNotWrap else IfWrap(id, ifWrap, ifNotWrap) - } - - private fun hasTrailingAffix(node: Node, next: Node): Boolean { - var n: Node? = next - while (n != null) { - if (n.type.isAffix && node.span.lineEnd == n.span.lineBegin) return true - n = n.children.getOrNull(0) - } - return false - } - - private fun modifierPrecedence(modifier: Node): Int { - return when (val text = modifier.text()) { - "abstract", - "open" -> 0 - "external" -> 1 - "local", - "hidden" -> 2 - "fixed", - "const" -> 3 - else -> throw RuntimeException("Unknown modifier `$text`") - } - } - - private fun isSameLineExpr(node: Node): Boolean { - return node.type in SAME_LINE_EXPRS - } - - private fun splitPrefixes(nodes: List): Pair, List> { - val splitPoint = nodes.indexOfFirst { !it.type.isAffix && it.type != NodeType.DOC_COMMENT } - return nodes.subList(0, splitPoint) to nodes.subList(splitPoint, nodes.size) - } - - private fun indentAfterFirstNewline( - nodes: List, - group: Boolean = false, - ): List { - val index = nodes.indexOfFirst { it is SpaceOrLine || it is ForceLine || it is Line } - if (index <= 0) return nodes - val indented = - if (group) { - group(Indent(nodes.subList(index, nodes.size))) - } else { - Indent(nodes.subList(index, nodes.size)) - } - - return nodes.subList(0, index) + listOf(indented) - } - - private fun groupOnSpace(fnodes: List): FormatNode { - val res = mutableListOf() - for ((i, node) in fnodes.withIndex()) { - if (i > 0 && (node is SpaceOrLine || node is Space)) { - res += groupOnSpace(fnodes.subList(i, fnodes.size)) - break - } else { - res += node - } - } - return Group(newId(), res) - } - - /** Flatten binary operators by precedence */ - private fun flattenBinaryOperatorExprs(node: Node): List { - val op = node.children.first { it.type == NodeType.OPERATOR }.text() - return flattenBinaryOperatorExprs(node, Operator.byName(op).prec) - } - - private fun flattenBinaryOperatorExprs(node: Node, prec: Int): List { - val actualOp = node.children.first { it.type == NodeType.OPERATOR }.text() - if (prec != Operator.byName(actualOp).prec) return listOf(node) - return buildList { - for (child in node.children) { - if (child.type == NodeType.BINARY_OP_EXPR) { - addAll(flattenBinaryOperatorExprs(child, prec)) - } else { - add(child) - } - } - } - } - - private fun Node.linesBetween(next: Node): Int = next.span.lineBegin - span.lineEnd - - private fun Node.text() = text(source) - - private fun Node.isTerminal(vararg texts: String): Boolean = - type == NodeType.TERMINAL && text(source) in texts - - private fun newId(): Int { - return id++ - } - - private fun nodes(vararg nodes: FormatNode) = Nodes(nodes.toList()) - - private fun group(vararg nodes: FormatNode) = Group(newId(), nodes.toList()) - - private fun indent(vararg nodes: FormatNode) = Indent(nodes.toList()) - - private class ImportComparator(private val source: CharArray) : Comparator { - override fun compare(o1: Node, o2: Node): Int { - val import1 = o1.findChildByType(NodeType.STRING_CHARS)?.text(source) - val import2 = o2.findChildByType(NodeType.STRING_CHARS)?.text(source) - if (import1 == null || import2 == null) { - // should never happen - throw RuntimeException("ImportComparator: not an import") - } - - return NaturalOrderComparator(ignoreCase = true).compare(import1, import2) - } - } - - private fun Node.firstProperChild(): Node? { - for (child in children) { - if (child.isProper()) return child - } - return null - } - - private fun List.lastProperNode(): Node? { - for (i in lastIndex downTo 0) { - if (this[i].isProper()) return this[i] - } - return null - } - - // returns true if this node is not an affix or terminal - private fun Node.isProper(): Boolean = !type.isAffix && type != NodeType.TERMINAL - - private fun Node.isMultiline(): Boolean = span.lineBegin < span.lineEnd - - private fun List.isMultiline(): Boolean = - if (isEmpty()) false else first().span.lineBegin < last().span.lineEnd - - private inline fun List.splitOn(pred: (T) -> Boolean): Pair, List> { - val index = indexOfFirst { pred(it) } - return if (index == -1) { - Pair(this, emptyList()) - } else { - Pair(take(index), drop(index)) - } - } - - class PeekableIterator(private val iterator: Iterator) : Iterator { - private var peek: T? = null - - private var hasPeek = false - - override fun next(): T { - return if (hasPeek) { - hasPeek = false - peek!! - } else { - iterator.next() - } - } - - override fun hasNext(): Boolean { - return if (hasPeek) true else iterator.hasNext() - } - - fun peek(): T { - if (!hasNext()) { - throw NoSuchElementException() - } - if (hasPeek) { - return peek!! - } - peek = iterator.next() - hasPeek = true - return peek!! - } - - inline fun takeUntilBefore(predicate: (T) -> Boolean): List { - return buildList { - while (true) { - if (!hasNext() || predicate(peek())) { - return@buildList - } - add(next()) - } - } - } - } - - private fun Iterator.peekable(): PeekableIterator { - return PeekableIterator(this) - } - - companion object { - private val ABSOLUTE_URL_REGEX = Regex("""\w+:.*""") - - private val TWO_NEWLINES = Nodes(listOf(ForceLine, ForceLine)) - - private val FORCE_LINE_AFFIXES = - EnumSet.of( - NodeType.DOC_COMMENT_LINE, - NodeType.LINE_COMMENT, - NodeType.SEMICOLON, - NodeType.SHEBANG, - ) - - private val EMPTY_SUFFIXES = - EnumSet.of( - NodeType.TYPE_ARGUMENT_LIST, - NodeType.TYPE_ANNOTATION, - NodeType.TYPE_PARAMETER_LIST, - NodeType.PARAMETER_LIST, - ) - - private val SAME_LINE_EXPRS = - EnumSet.of(NodeType.NEW_EXPR, NodeType.AMENDS_EXPR, NodeType.FUNCTION_LITERAL_EXPR) - } -} diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt deleted file mode 100644 index 663fdcb8..00000000 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Formatter.kt +++ /dev/null @@ -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 } - } -} diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt deleted file mode 100644 index 1d80bc61..00000000 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt +++ /dev/null @@ -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 = 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 = " " - } -} diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/NaturalOrderComparator.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/NaturalOrderComparator.kt deleted file mode 100644 index ab5e70fa..00000000 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/NaturalOrderComparator.kt +++ /dev/null @@ -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 { - - 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) -} diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt deleted file mode 100644 index 6412c55d..00000000 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt +++ /dev/null @@ -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 = - 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 - -data class Nodes(val nodes: List) : FormatNode - -data class Group(val id: Int, val nodes: List) : FormatNode - -data class MultilineStringGroup(val endQuoteCol: Int, val nodes: List) : FormatNode - -data class IfWrap(val id: Int, val ifWrap: FormatNode, val ifNotWrap: FormatNode) : FormatNode