mirror of
https://github.com/apple/pkl.git
synced 2026-03-20 00:04:05 +01:00
Implement canonical formatter (#1107)
CLI commands also added: `pkl format check` and `pkl format apply`.
This commit is contained in:
1574
pkl-parser/src/main/java/org/pkl/parser/GenericParser.java
Normal file
1574
pkl-parser/src/main/java/org/pkl/parser/GenericParser.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.parser;
|
||||
|
||||
import org.pkl.parser.syntax.generic.FullSpan;
|
||||
|
||||
public class GenericParserError extends RuntimeException {
|
||||
private final FullSpan span;
|
||||
|
||||
public GenericParserError(String msg, FullSpan span) {
|
||||
super(msg);
|
||||
this.span = span;
|
||||
}
|
||||
|
||||
public FullSpan getSpan() {
|
||||
return span;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getMessage() + " at " + span;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package org.pkl.parser;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import org.pkl.parser.syntax.generic.FullSpan;
|
||||
import org.pkl.parser.util.ErrorMessages;
|
||||
|
||||
public class Lexer {
|
||||
@@ -25,6 +26,10 @@ public class Lexer {
|
||||
private final int size;
|
||||
protected int cursor = 0;
|
||||
protected int sCursor = 0;
|
||||
private int line = 1;
|
||||
private int sLine = 1;
|
||||
private int col = 1;
|
||||
private int sCol = 1;
|
||||
private char lookahead;
|
||||
private State state = State.DEFAULT;
|
||||
private final Deque<InterpolationScope> interpolationStack = new ArrayDeque<>();
|
||||
@@ -50,6 +55,11 @@ public class Lexer {
|
||||
return new Span(sCursor, cursor - sCursor);
|
||||
}
|
||||
|
||||
// The full span of the last lexed token
|
||||
public FullSpan fullSpan() {
|
||||
return new FullSpan(sCursor, cursor - sCursor, sLine, sCol, line, col);
|
||||
}
|
||||
|
||||
// The text of the last lexed token
|
||||
public String text() {
|
||||
return new String(source, sCursor, cursor - sCursor);
|
||||
@@ -65,6 +75,8 @@ public class Lexer {
|
||||
|
||||
public Token next() {
|
||||
sCursor = cursor;
|
||||
sLine = line;
|
||||
sCol = col;
|
||||
newLinesBetween = 0;
|
||||
return switch (state) {
|
||||
case DEFAULT -> nextDefault();
|
||||
@@ -79,7 +91,9 @@ public class Lexer {
|
||||
sCursor = cursor;
|
||||
if (ch == '\n') {
|
||||
newLinesBetween++;
|
||||
sLine = line;
|
||||
}
|
||||
sCol = col;
|
||||
ch = nextChar();
|
||||
}
|
||||
return switch (ch) {
|
||||
@@ -678,15 +692,23 @@ public class Lexer {
|
||||
} else {
|
||||
lookahead = source[cursor];
|
||||
}
|
||||
if (tmp == '\n') {
|
||||
line++;
|
||||
col = 1;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
private void backup() {
|
||||
lookahead = source[--cursor];
|
||||
col--;
|
||||
}
|
||||
|
||||
private void backup(int amount) {
|
||||
cursor -= amount;
|
||||
col -= amount;
|
||||
lookahead = source[cursor];
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.pkl.parser;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import org.pkl.parser.syntax.Annotation;
|
||||
@@ -1809,16 +1808,13 @@ public class Parser {
|
||||
private FullToken forceNext() {
|
||||
var tk = lexer.next();
|
||||
precededBySemicolon = false;
|
||||
while (AFFIXES.contains(tk)) {
|
||||
while (tk.isAffix()) {
|
||||
precededBySemicolon = precededBySemicolon || tk == Token.SEMICOLON;
|
||||
tk = lexer.next();
|
||||
}
|
||||
return new FullToken(tk, lexer.span(), lexer.newLinesBetween);
|
||||
}
|
||||
|
||||
private static final EnumSet<Token> AFFIXES =
|
||||
EnumSet.of(Token.LINE_COMMENT, Token.BLOCK_COMMENT, Token.SEMICOLON);
|
||||
|
||||
// Like next, but don't ignore comments
|
||||
private FullToken nextComment() {
|
||||
prev = _lookahead;
|
||||
|
||||
@@ -191,6 +191,13 @@ public enum Token {
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isAffix() {
|
||||
return switch (this) {
|
||||
case LINE_COMMENT, BLOCK_COMMENT, SEMICOLON -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public String text() {
|
||||
if (this == UNDERSCORE) {
|
||||
return "_";
|
||||
|
||||
@@ -35,8 +35,10 @@ public enum Operator {
|
||||
INT_DIV(9, true),
|
||||
MOD(9, true),
|
||||
POW(10, false),
|
||||
DOT(11, true),
|
||||
QDOT(11, true);
|
||||
NON_NULL(16, true),
|
||||
SUBSCRIPT(18, true),
|
||||
DOT(20, true),
|
||||
QDOT(20, true);
|
||||
|
||||
private final int prec;
|
||||
private final boolean isLeftAssoc;
|
||||
@@ -53,4 +55,33 @@ public enum Operator {
|
||||
public boolean isLeftAssoc() {
|
||||
return isLeftAssoc;
|
||||
}
|
||||
|
||||
public static Operator byName(String name) {
|
||||
return switch (name) {
|
||||
case "??" -> NULL_COALESCE;
|
||||
case "|>" -> PIPE;
|
||||
case "||" -> OR;
|
||||
case "&&" -> AND;
|
||||
case "==" -> EQ_EQ;
|
||||
case "!=" -> NOT_EQ;
|
||||
case "is" -> IS;
|
||||
case "as" -> AS;
|
||||
case "<" -> LT;
|
||||
case "<=" -> LTE;
|
||||
case ">" -> GT;
|
||||
case ">=" -> GTE;
|
||||
case "+" -> PLUS;
|
||||
case "-" -> MINUS;
|
||||
case "*" -> MULT;
|
||||
case "/" -> DIV;
|
||||
case "~/" -> INT_DIV;
|
||||
case "%" -> MOD;
|
||||
case "**" -> POW;
|
||||
case "!!" -> NON_NULL;
|
||||
case "[" -> SUBSCRIPT;
|
||||
case "." -> DOT;
|
||||
case "?." -> QDOT;
|
||||
default -> throw new RuntimeException("Unknown operator: " + name);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.parser.syntax.generic;
|
||||
|
||||
import org.pkl.parser.Span;
|
||||
|
||||
public record FullSpan(
|
||||
int charIndex, int length, int lineBegin, int colBegin, int lineEnd, int colEnd) {
|
||||
|
||||
public FullSpan endWith(FullSpan end) {
|
||||
return new FullSpan(
|
||||
charIndex,
|
||||
end.charIndex - charIndex + end.length,
|
||||
lineBegin,
|
||||
colBegin,
|
||||
end.lineEnd,
|
||||
end.colEnd);
|
||||
}
|
||||
|
||||
public Span toSpan() {
|
||||
return new Span(charIndex, length);
|
||||
}
|
||||
|
||||
public boolean sameLine(FullSpan other) {
|
||||
return lineEnd == other.lineBegin;
|
||||
}
|
||||
|
||||
public FullSpan stopSpan() {
|
||||
return new FullSpan(charIndex + length - 1, 1, lineEnd, colEnd, lineEnd, colEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(%d:%d - %d:%d)".formatted(lineBegin, colBegin, lineEnd, colEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.parser.syntax.generic;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.pkl.parser.util.Nullable;
|
||||
|
||||
public class Node {
|
||||
public final List<Node> children;
|
||||
public final FullSpan span;
|
||||
public final NodeType type;
|
||||
private @Nullable String text;
|
||||
|
||||
public Node(NodeType type, FullSpan span) {
|
||||
this(type, span, Collections.emptyList());
|
||||
}
|
||||
|
||||
public Node(NodeType type, FullSpan span, List<Node> children) {
|
||||
this.type = type;
|
||||
this.span = span;
|
||||
this.children = Collections.unmodifiableList(children);
|
||||
}
|
||||
|
||||
public Node(NodeType type, List<Node> children) {
|
||||
this.type = type;
|
||||
if (children.isEmpty()) throw new RuntimeException("No children or span given for node");
|
||||
var end = children.get(children.size() - 1).span;
|
||||
this.span = children.get(0).span.endWith(end);
|
||||
this.children = Collections.unmodifiableList(children);
|
||||
}
|
||||
|
||||
public String text(char[] source) {
|
||||
if (text == null) {
|
||||
text = new String(source, span.charIndex(), span.length());
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/** Returns the first child of type {@code type} or {@code null}. */
|
||||
public @Nullable Node findChildByType(NodeType type) {
|
||||
for (var child : children) {
|
||||
if (child.type == type) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Node node = (Node) o;
|
||||
return Objects.equals(children, node.children)
|
||||
&& Objects.equals(span, node.span)
|
||||
&& Objects.equals(type, node.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(children, span, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Node{type='" + type + "', span=" + span + ", children=" + children + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.parser.syntax.generic;
|
||||
|
||||
public enum NodeType {
|
||||
TERMINAL,
|
||||
SHEBANG,
|
||||
// affixes,
|
||||
LINE_COMMENT(NodeKind.AFFIX),
|
||||
BLOCK_COMMENT(NodeKind.AFFIX),
|
||||
SEMICOLON(NodeKind.AFFIX),
|
||||
|
||||
MODULE,
|
||||
DOC_COMMENT,
|
||||
DOC_COMMENT_LINE,
|
||||
MODIFIER,
|
||||
MODIFIER_LIST,
|
||||
AMENDS_CLAUSE,
|
||||
EXTENDS_CLAUSE,
|
||||
MODULE_DECLARATION,
|
||||
MODULE_DEFINITION,
|
||||
ANNOTATION,
|
||||
IDENTIFIER,
|
||||
QUALIFIED_IDENTIFIER,
|
||||
IMPORT,
|
||||
IMPORT_ALIAS,
|
||||
IMPORT_LIST,
|
||||
TYPEALIAS,
|
||||
TYPEALIAS_HEADER,
|
||||
TYPEALIAS_BODY,
|
||||
CLASS,
|
||||
CLASS_HEADER,
|
||||
CLASS_HEADER_EXTENDS,
|
||||
CLASS_BODY,
|
||||
CLASS_BODY_ELEMENTS,
|
||||
CLASS_METHOD,
|
||||
CLASS_METHOD_HEADER,
|
||||
CLASS_METHOD_BODY,
|
||||
CLASS_PROPERTY,
|
||||
CLASS_PROPERTY_HEADER,
|
||||
CLASS_PROPERTY_HEADER_BEGIN,
|
||||
CLASS_PROPERTY_BODY,
|
||||
OBJECT_BODY,
|
||||
OBJECT_MEMBER_LIST,
|
||||
PARAMETER,
|
||||
TYPE_ANNOTATION,
|
||||
PARAMETER_LIST,
|
||||
PARAMETER_LIST_ELEMENTS,
|
||||
TYPE_PARAMETER_LIST,
|
||||
TYPE_PARAMETER_LIST_ELEMENTS,
|
||||
ARGUMENT_LIST,
|
||||
ARGUMENT_LIST_ELEMENTS,
|
||||
TYPE_ARGUMENT_LIST,
|
||||
TYPE_ARGUMENT_LIST_ELEMENTS,
|
||||
OBJECT_PARAMETER_LIST,
|
||||
TYPE_PARAMETER,
|
||||
STRING_CONSTANT,
|
||||
OPERATOR,
|
||||
STRING_NEWLINE,
|
||||
STRING_ESCAPE,
|
||||
|
||||
// members
|
||||
OBJECT_ELEMENT,
|
||||
OBJECT_PROPERTY,
|
||||
OBJECT_PROPERTY_HEADER,
|
||||
OBJECT_PROPERTY_HEADER_BEGIN,
|
||||
OBJECT_PROPERTY_BODY,
|
||||
OBJECT_METHOD,
|
||||
MEMBER_PREDICATE,
|
||||
OBJECT_ENTRY,
|
||||
OBJECT_ENTRY_HEADER,
|
||||
OBJECT_SPREAD,
|
||||
WHEN_GENERATOR,
|
||||
WHEN_GENERATOR_HEADER,
|
||||
FOR_GENERATOR,
|
||||
FOR_GENERATOR_HEADER,
|
||||
FOR_GENERATOR_HEADER_DEFINITION,
|
||||
FOR_GENERATOR_HEADER_DEFINITION_HEADER,
|
||||
|
||||
// expressions
|
||||
THIS_EXPR(NodeKind.EXPR),
|
||||
OUTER_EXPR(NodeKind.EXPR),
|
||||
MODULE_EXPR(NodeKind.EXPR),
|
||||
NULL_EXPR(NodeKind.EXPR),
|
||||
THROW_EXPR(NodeKind.EXPR),
|
||||
TRACE_EXPR(NodeKind.EXPR),
|
||||
IMPORT_EXPR(NodeKind.EXPR),
|
||||
READ_EXPR(NodeKind.EXPR),
|
||||
NEW_EXPR(NodeKind.EXPR),
|
||||
NEW_HEADER,
|
||||
UNARY_MINUS_EXPR(NodeKind.EXPR),
|
||||
LOGICAL_NOT_EXPR(NodeKind.EXPR),
|
||||
FUNCTION_LITERAL_EXPR(NodeKind.EXPR),
|
||||
FUNCTION_LITERAL_BODY,
|
||||
PARENTHESIZED_EXPR(NodeKind.EXPR),
|
||||
PARENTHESIZED_EXPR_ELEMENTS,
|
||||
SUPER_SUBSCRIPT_EXPR(NodeKind.EXPR),
|
||||
SUPER_ACCESS_EXPR(NodeKind.EXPR),
|
||||
SUBSCRIPT_EXPR(NodeKind.EXPR),
|
||||
IF_EXPR(NodeKind.EXPR),
|
||||
IF_HEADER,
|
||||
IF_CONDITION,
|
||||
IF_CONDITION_EXPR,
|
||||
IF_THEN_EXPR,
|
||||
IF_ELSE_EXPR,
|
||||
LET_EXPR(NodeKind.EXPR),
|
||||
LET_PARAMETER_DEFINITION,
|
||||
LET_PARAMETER,
|
||||
BOOL_LITERAL_EXPR(NodeKind.EXPR),
|
||||
INT_LITERAL_EXPR(NodeKind.EXPR),
|
||||
FLOAT_LITERAL_EXPR(NodeKind.EXPR),
|
||||
SINGLE_LINE_STRING_LITERAL_EXPR(NodeKind.EXPR),
|
||||
MULTI_LINE_STRING_LITERAL_EXPR(NodeKind.EXPR),
|
||||
UNQUALIFIED_ACCESS_EXPR(NodeKind.EXPR),
|
||||
NON_NULL_EXPR(NodeKind.EXPR),
|
||||
AMENDS_EXPR(NodeKind.EXPR),
|
||||
BINARY_OP_EXPR(NodeKind.EXPR),
|
||||
|
||||
// types
|
||||
UNKNOWN_TYPE(NodeKind.TYPE),
|
||||
NOTHING_TYPE(NodeKind.TYPE),
|
||||
MODULE_TYPE(NodeKind.TYPE),
|
||||
UNION_TYPE(NodeKind.TYPE),
|
||||
FUNCTION_TYPE(NodeKind.TYPE),
|
||||
FUNCTION_TYPE_PARAMETERS,
|
||||
PARENTHESIZED_TYPE(NodeKind.TYPE),
|
||||
PARENTHESIZED_TYPE_ELEMENTS,
|
||||
DECLARED_TYPE(NodeKind.TYPE),
|
||||
NULLABLE_TYPE(NodeKind.TYPE),
|
||||
STRING_CONSTANT_TYPE(NodeKind.TYPE),
|
||||
CONSTRAINED_TYPE(NodeKind.TYPE),
|
||||
CONSTRAINED_TYPE_CONSTRAINT,
|
||||
CONSTRAINED_TYPE_ELEMENTS;
|
||||
|
||||
private final NodeKind kind;
|
||||
|
||||
NodeType() {
|
||||
this.kind = NodeKind.NONE;
|
||||
}
|
||||
|
||||
NodeType(NodeKind kind) {
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
public boolean isAffix() {
|
||||
return kind == NodeKind.AFFIX;
|
||||
}
|
||||
|
||||
public boolean isExpression() {
|
||||
return kind == NodeKind.EXPR;
|
||||
}
|
||||
|
||||
public boolean isType() {
|
||||
return kind == NodeKind.TYPE;
|
||||
}
|
||||
|
||||
private enum NodeKind {
|
||||
TYPE,
|
||||
EXPR,
|
||||
AFFIX,
|
||||
NONE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NonnullByDefault
|
||||
package org.pkl.parser.syntax.generic;
|
||||
|
||||
import org.pkl.parser.util.NonnullByDefault;
|
||||
261
pkl-parser/src/test/kotlin/org/pkl/parser/GenericSexpRenderer.kt
Normal file
261
pkl-parser/src/test/kotlin/org/pkl/parser/GenericSexpRenderer.kt
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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.parser
|
||||
|
||||
import java.util.EnumSet
|
||||
import org.pkl.parser.syntax.generic.Node
|
||||
import org.pkl.parser.syntax.generic.NodeType
|
||||
|
||||
class GenericSexpRenderer(code: String) {
|
||||
private var tab = ""
|
||||
private var buf = StringBuilder()
|
||||
private val source = code.toCharArray()
|
||||
|
||||
fun render(node: Node): String {
|
||||
innerRender(node)
|
||||
return buf.toString()
|
||||
}
|
||||
|
||||
private fun innerRender(node: Node) {
|
||||
if (node.type == NodeType.UNION_TYPE) {
|
||||
renderUnionType(node)
|
||||
return
|
||||
}
|
||||
if (node.type == NodeType.BINARY_OP_EXPR && binopName(node).endsWith("ualifiedAccessExpr")) {
|
||||
renderQualifiedAccess(node)
|
||||
return
|
||||
}
|
||||
doRender(name(node), collectChildren(node))
|
||||
}
|
||||
|
||||
private fun doRender(name: String, children: List<Node>) {
|
||||
buf.append(tab)
|
||||
buf.append("(")
|
||||
buf.append(name)
|
||||
val oldTab = increaseTab()
|
||||
for (child in children) {
|
||||
buf.append('\n')
|
||||
innerRender(child)
|
||||
}
|
||||
tab = oldTab
|
||||
buf.append(')')
|
||||
}
|
||||
|
||||
private fun renderUnionType(node: Node) {
|
||||
buf.append(tab)
|
||||
buf.append("(")
|
||||
buf.append(name(node))
|
||||
val oldTab = increaseTab()
|
||||
var previousTerminal: Node? = null
|
||||
for (child in node.children) {
|
||||
if (child.type == NodeType.TERMINAL) previousTerminal = child
|
||||
if (child.type in IGNORED_CHILDREN) continue
|
||||
buf.append('\n')
|
||||
if (previousTerminal != null && previousTerminal.text(source) == "*") {
|
||||
previousTerminal = null
|
||||
renderDefaultUnionType(child)
|
||||
} else {
|
||||
innerRender(child)
|
||||
}
|
||||
}
|
||||
tab = oldTab
|
||||
buf.append(')')
|
||||
}
|
||||
|
||||
private fun renderQualifiedAccess(node: Node) {
|
||||
var children = node.children
|
||||
if (children.last().type == NodeType.UNQUALIFIED_ACCESS_EXPR) {
|
||||
children = children.dropLast(1) + collectChildren(children.last())
|
||||
}
|
||||
val toRender = mutableListOf<Node>()
|
||||
for (child in children) {
|
||||
if (child.type in IGNORED_CHILDREN || child.type == NodeType.OPERATOR) continue
|
||||
toRender += child
|
||||
}
|
||||
doRender(name(node), toRender)
|
||||
}
|
||||
|
||||
private fun renderDefaultUnionType(node: Node) {
|
||||
buf.append(tab)
|
||||
buf.append("(defaultUnionType\n")
|
||||
val oldTab = increaseTab()
|
||||
innerRender(node)
|
||||
tab = oldTab
|
||||
buf.append(')')
|
||||
}
|
||||
|
||||
private fun collectChildren(node: Node): List<Node> =
|
||||
when (node.type) {
|
||||
NodeType.MULTI_LINE_STRING_LITERAL_EXPR ->
|
||||
node.children.filter { it.type !in IGNORED_CHILDREN && !it.type.isStringData() }
|
||||
NodeType.SINGLE_LINE_STRING_LITERAL_EXPR -> {
|
||||
val children = node.children.filter { it.type !in IGNORED_CHILDREN }
|
||||
val res = mutableListOf<Node>()
|
||||
var prev: Node? = null
|
||||
for (child in children) {
|
||||
val inARow = child.type.isStringData() && (prev != null && prev.type.isStringData())
|
||||
if (!inARow) {
|
||||
res += child
|
||||
}
|
||||
prev = child
|
||||
}
|
||||
res
|
||||
}
|
||||
NodeType.DOC_COMMENT -> listOf()
|
||||
else -> {
|
||||
val nodes = mutableListOf<Node>()
|
||||
for (child in node.children) {
|
||||
if (child.type in IGNORED_CHILDREN) continue
|
||||
if (child.type in UNPACK_CHILDREN) {
|
||||
nodes += collectChildren(child)
|
||||
} else {
|
||||
nodes += child
|
||||
}
|
||||
}
|
||||
nodes
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeType.isStringData(): Boolean =
|
||||
this == NodeType.STRING_CONSTANT || this == NodeType.STRING_ESCAPE
|
||||
|
||||
private fun name(node: Node): String =
|
||||
when (node.type) {
|
||||
NodeType.MODULE_DECLARATION -> "moduleHeader"
|
||||
NodeType.IMPORT -> importName(node, isExpr = false)
|
||||
NodeType.IMPORT_EXPR -> importName(node, isExpr = true)
|
||||
NodeType.BINARY_OP_EXPR -> binopName(node)
|
||||
NodeType.CLASS -> "clazz"
|
||||
NodeType.EXTENDS_CLAUSE,
|
||||
NodeType.AMENDS_CLAUSE -> "extendsOrAmendsClause"
|
||||
NodeType.TYPEALIAS -> "typeAlias"
|
||||
NodeType.STRING_ESCAPE -> "stringConstant"
|
||||
NodeType.READ_EXPR -> {
|
||||
val terminal = node.children.find { it.type == NodeType.TERMINAL }!!.text(source)
|
||||
when (terminal) {
|
||||
"read*" -> "readGlobExpr"
|
||||
"read?" -> "readNullExpr"
|
||||
else -> "readExpr"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val names = node.type.name.split('_').map { it.lowercase() }
|
||||
if (names.size > 1) {
|
||||
val capitalized = names.drop(1).map { n -> n.replaceFirstChar { it.titlecase() } }
|
||||
(listOf(names[0]) + capitalized).joinToString("")
|
||||
} else names[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun importName(node: Node, isExpr: Boolean): String {
|
||||
val terminal = node.children.find { it.type == NodeType.TERMINAL }!!.text(source)
|
||||
val suffix = if (isExpr) "Expr" else "Clause"
|
||||
return if (terminal == "import*") "importGlob$suffix" else "import$suffix"
|
||||
}
|
||||
|
||||
private fun binopName(node: Node): String {
|
||||
val op = node.children.find { it.type == NodeType.OPERATOR }!!.text(source)
|
||||
return when (op) {
|
||||
"**" -> "exponentiationExpr"
|
||||
"*",
|
||||
"/",
|
||||
"~/",
|
||||
"%" -> "multiplicativeExpr"
|
||||
"+",
|
||||
"-" -> "additiveExpr"
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=" -> "comparisonExpr"
|
||||
"is" -> "typeCheckExpr"
|
||||
"as" -> "typeCastExpr"
|
||||
"==",
|
||||
"!=" -> "equalityExpr"
|
||||
"&&" -> "logicalAndExpr"
|
||||
"||" -> "logicalOrExpr"
|
||||
"|>" -> "pipeExpr"
|
||||
"??" -> "nullCoalesceExpr"
|
||||
"." -> "qualifiedAccessExpr"
|
||||
"?." -> "nullableQualifiedAccessExpr"
|
||||
else -> throw RuntimeException("Unknown operator: $op")
|
||||
}
|
||||
}
|
||||
|
||||
private fun increaseTab(): String {
|
||||
val old = tab
|
||||
tab += " "
|
||||
return old
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IGNORED_CHILDREN =
|
||||
EnumSet.of(
|
||||
NodeType.LINE_COMMENT,
|
||||
NodeType.BLOCK_COMMENT,
|
||||
NodeType.SHEBANG,
|
||||
NodeType.SEMICOLON,
|
||||
NodeType.TERMINAL,
|
||||
NodeType.OPERATOR,
|
||||
NodeType.STRING_NEWLINE,
|
||||
)
|
||||
|
||||
private val UNPACK_CHILDREN =
|
||||
EnumSet.of(
|
||||
NodeType.MODULE_DEFINITION,
|
||||
NodeType.IMPORT_LIST,
|
||||
NodeType.IMPORT_ALIAS,
|
||||
NodeType.TYPEALIAS_HEADER,
|
||||
NodeType.TYPEALIAS_BODY,
|
||||
NodeType.CLASS_PROPERTY_HEADER,
|
||||
NodeType.CLASS_PROPERTY_HEADER_BEGIN,
|
||||
NodeType.CLASS_PROPERTY_BODY,
|
||||
NodeType.CLASS_METHOD_HEADER,
|
||||
NodeType.CLASS_METHOD_BODY,
|
||||
NodeType.CLASS_HEADER,
|
||||
NodeType.CLASS_HEADER_EXTENDS,
|
||||
NodeType.CLASS_BODY_ELEMENTS,
|
||||
NodeType.MODIFIER_LIST,
|
||||
NodeType.NEW_HEADER,
|
||||
NodeType.OBJECT_MEMBER_LIST,
|
||||
NodeType.OBJECT_ENTRY_HEADER,
|
||||
NodeType.OBJECT_PROPERTY_HEADER,
|
||||
NodeType.OBJECT_PROPERTY_HEADER_BEGIN,
|
||||
NodeType.OBJECT_PROPERTY_BODY,
|
||||
NodeType.OBJECT_PARAMETER_LIST,
|
||||
NodeType.FOR_GENERATOR_HEADER,
|
||||
NodeType.FOR_GENERATOR_HEADER_DEFINITION,
|
||||
NodeType.FOR_GENERATOR_HEADER_DEFINITION_HEADER,
|
||||
NodeType.WHEN_GENERATOR_HEADER,
|
||||
NodeType.IF_HEADER,
|
||||
NodeType.IF_CONDITION,
|
||||
NodeType.IF_CONDITION_EXPR,
|
||||
NodeType.IF_THEN_EXPR,
|
||||
NodeType.IF_ELSE_EXPR,
|
||||
NodeType.FUNCTION_LITERAL_BODY,
|
||||
NodeType.ARGUMENT_LIST_ELEMENTS,
|
||||
NodeType.PARAMETER_LIST_ELEMENTS,
|
||||
NodeType.CONSTRAINED_TYPE_CONSTRAINT,
|
||||
NodeType.CONSTRAINED_TYPE_ELEMENTS,
|
||||
NodeType.TYPE_PARAMETER_LIST_ELEMENTS,
|
||||
NodeType.TYPE_ARGUMENT_LIST_ELEMENTS,
|
||||
NodeType.LET_PARAMETER_DEFINITION,
|
||||
NodeType.LET_PARAMETER,
|
||||
NodeType.PARENTHESIZED_EXPR_ELEMENTS,
|
||||
NodeType.PARENTHESIZED_TYPE_ELEMENTS,
|
||||
NodeType.FUNCTION_TYPE_PARAMETERS,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.parser
|
||||
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.io.path.readText
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.SoftAssertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import org.pkl.commons.walk
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class ParserComparisonTest {
|
||||
|
||||
@Test
|
||||
fun compareSnippetTests() {
|
||||
SoftAssertions.assertSoftly { softly ->
|
||||
getSnippets()
|
||||
.parallelStream()
|
||||
.map { Pair(it.pathString, it.readText()) }
|
||||
.forEach { (path, snippet) ->
|
||||
try {
|
||||
compare(snippet, path, softly)
|
||||
} catch (e: GenericParserError) {
|
||||
softly.fail("path: $path. Message: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSnippets(): List<Path> {
|
||||
return Path("../pkl-core/src/test/files/LanguageSnippetTests/input")
|
||||
.walk()
|
||||
.filter { path ->
|
||||
val pathStr = path.toString().replace("\\", "/")
|
||||
path.extension == "pkl" &&
|
||||
!exceptions.any { pathStr.endsWith(it) } &&
|
||||
!regexExceptions.any { it.matches(pathStr) }
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun compare(code: String, path: String? = null, softly: SoftAssertions? = null) {
|
||||
val (sexp, genSexp) = renderBoth(code)
|
||||
when {
|
||||
(path != null && softly != null) ->
|
||||
softly.assertThat(genSexp).`as`("path: $path").isEqualTo(sexp)
|
||||
else -> assertThat(genSexp).`as`("path: $path").isEqualTo(sexp)
|
||||
}
|
||||
}
|
||||
|
||||
fun renderBoth(code: String): Pair<String, String> =
|
||||
Pair(renderCode(code), renderGenericCode(code))
|
||||
|
||||
companion object {
|
||||
private fun renderCode(code: String): String {
|
||||
val parser = Parser()
|
||||
val mod = parser.parseModule(code)
|
||||
val renderer = SexpRenderer()
|
||||
return renderer.render(mod)
|
||||
}
|
||||
|
||||
private fun renderGenericCode(code: String): String {
|
||||
val parser = GenericParser()
|
||||
val mod = parser.parseModule(code)
|
||||
val renderer = GenericSexpRenderer(code)
|
||||
return renderer.render(mod)
|
||||
}
|
||||
|
||||
// tests that are not syntactically valid Pkl
|
||||
private val exceptions =
|
||||
setOf(
|
||||
"stringError1.pkl",
|
||||
"annotationIsNotExpression2.pkl",
|
||||
"amendsRequiresParens.pkl",
|
||||
"errors/parser18.pkl",
|
||||
"errors/nested1.pkl",
|
||||
"errors/invalidCharacterEscape.pkl",
|
||||
"errors/invalidUnicodeEscape.pkl",
|
||||
"errors/unterminatedUnicodeEscape.pkl",
|
||||
"errors/keywordNotAllowedHere1.pkl",
|
||||
"errors/keywordNotAllowedHere2.pkl",
|
||||
"errors/keywordNotAllowedHere3.pkl",
|
||||
"errors/keywordNotAllowedHere4.pkl",
|
||||
"errors/moduleWithHighMinPklVersionAndParseErrors.pkl",
|
||||
"errors/underscore.pkl",
|
||||
"errors/shebang.pkl",
|
||||
"notAUnionDefault.pkl",
|
||||
"multipleDefaults.pkl",
|
||||
"modules/invalidModule1.pkl",
|
||||
)
|
||||
|
||||
private val regexExceptions =
|
||||
setOf(
|
||||
Regex(".*/errors/delimiters/.*"),
|
||||
Regex(".*/errors/parser\\d+\\.pkl"),
|
||||
Regex(".*/parser/.*"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,13 +82,22 @@ class SexpRenderer {
|
||||
}
|
||||
if (decl.extendsOrAmendsDecl !== null) {
|
||||
buf.append('\n')
|
||||
buf.append(tab)
|
||||
buf.append("(extendsOrAmendsClause)")
|
||||
renderExtendsOrAmendsClause(decl.extendsOrAmendsDecl!!)
|
||||
}
|
||||
tab = oldTab
|
||||
buf.append(')')
|
||||
}
|
||||
|
||||
fun renderExtendsOrAmendsClause(clause: ExtendsOrAmendsClause) {
|
||||
buf.append(tab)
|
||||
buf.append("(extendsOrAmendsClause")
|
||||
val oldTab = increaseTab()
|
||||
buf.append('\n')
|
||||
renderStringConstant(clause.url)
|
||||
tab = oldTab
|
||||
buf.append(')')
|
||||
}
|
||||
|
||||
fun renderImport(imp: ImportClause) {
|
||||
buf.append(tab)
|
||||
if (imp.isGlob) {
|
||||
@@ -97,6 +106,8 @@ class SexpRenderer {
|
||||
buf.append("(importClause")
|
||||
}
|
||||
val oldTab = increaseTab()
|
||||
buf.append('\n')
|
||||
renderStringConstant(imp.importStr)
|
||||
if (imp.alias !== null) {
|
||||
buf.append('\n')
|
||||
buf.append(tab)
|
||||
@@ -178,6 +189,7 @@ class SexpRenderer {
|
||||
buf.append("(identifier)")
|
||||
val tparList = `typealias`.typeParameterList
|
||||
if (tparList !== null) {
|
||||
buf.append('\n')
|
||||
renderTypeParameterList(tparList)
|
||||
}
|
||||
buf.append('\n')
|
||||
@@ -244,6 +256,7 @@ class SexpRenderer {
|
||||
buf.append("(identifier)")
|
||||
val tparList = classMethod.typeParameterList
|
||||
if (tparList !== null) {
|
||||
buf.append('\n')
|
||||
renderTypeParameterList(tparList)
|
||||
}
|
||||
buf.append('\n')
|
||||
@@ -385,11 +398,7 @@ class SexpRenderer {
|
||||
is MultiLineStringLiteralExpr -> renderMultiLineStringLiteral(expr)
|
||||
is ThrowExpr -> renderThrowExpr(expr)
|
||||
is TraceExpr -> renderTraceExpr(expr)
|
||||
is ImportExpr -> {
|
||||
buf.append(tab)
|
||||
val name = if (expr.isGlob) "(importGlobExpr)" else "(importExpr)"
|
||||
buf.append(name)
|
||||
}
|
||||
is ImportExpr -> renderImportExpr(expr)
|
||||
is ReadExpr -> renderReadExpr(expr)
|
||||
is UnqualifiedAccessExpr -> renderUnqualifiedAccessExpr(expr)
|
||||
is QualifiedAccessExpr -> renderQualifiedAccessExpr(expr)
|
||||
@@ -399,7 +408,7 @@ class SexpRenderer {
|
||||
is IfExpr -> renderIfExpr(expr)
|
||||
is LetExpr -> renderLetExpr(expr)
|
||||
is FunctionLiteralExpr -> renderFunctionLiteralExpr(expr)
|
||||
is ParenthesizedExpr -> renderParenthesisedExpr(expr)
|
||||
is ParenthesizedExpr -> renderParenthesizedExpr(expr)
|
||||
is NewExpr -> renderNewExpr(expr)
|
||||
is AmendsExpr -> renderAmendsExpr(expr)
|
||||
is NonNullExpr -> renderNonNullExpr(expr)
|
||||
@@ -421,7 +430,7 @@ class SexpRenderer {
|
||||
renderExpr(part.expr)
|
||||
} else {
|
||||
buf.append('\n').append(tab)
|
||||
buf.append("(stringConstantExpr)")
|
||||
buf.append("(stringConstant)")
|
||||
}
|
||||
}
|
||||
buf.append(')')
|
||||
@@ -480,6 +489,17 @@ class SexpRenderer {
|
||||
tab = oldTab
|
||||
}
|
||||
|
||||
fun renderImportExpr(expr: ImportExpr) {
|
||||
buf.append(tab)
|
||||
val name = if (expr.isGlob) "(importGlobExpr" else "(importExpr"
|
||||
buf.append(name)
|
||||
val oldTab = increaseTab()
|
||||
buf.append('\n')
|
||||
renderStringConstant(expr.importStr)
|
||||
buf.append(')')
|
||||
tab = oldTab
|
||||
}
|
||||
|
||||
fun renderUnqualifiedAccessExpr(expr: UnqualifiedAccessExpr) {
|
||||
buf.append(tab)
|
||||
buf.append("(unqualifiedAccessExpr")
|
||||
@@ -517,6 +537,7 @@ class SexpRenderer {
|
||||
buf.append("(superAccessExpr")
|
||||
val oldTab = increaseTab()
|
||||
buf.append('\n')
|
||||
buf.append(tab)
|
||||
buf.append("(identifier)")
|
||||
if (expr.argumentList !== null) {
|
||||
buf.append('\n')
|
||||
@@ -588,7 +609,7 @@ class SexpRenderer {
|
||||
tab = oldTab
|
||||
}
|
||||
|
||||
fun renderParenthesisedExpr(expr: ParenthesizedExpr) {
|
||||
fun renderParenthesizedExpr(expr: ParenthesizedExpr) {
|
||||
buf.append(tab)
|
||||
buf.append("(parenthesizedExpr")
|
||||
val oldTab = increaseTab()
|
||||
@@ -713,6 +734,11 @@ class SexpRenderer {
|
||||
tab = oldTab
|
||||
}
|
||||
|
||||
fun renderStringConstant(str: StringConstant) {
|
||||
buf.append(tab)
|
||||
buf.append("(stringConstant)")
|
||||
}
|
||||
|
||||
fun renderTypeAnnotation(typeAnnotation: TypeAnnotation) {
|
||||
buf.append(tab)
|
||||
buf.append("(typeAnnotation")
|
||||
@@ -737,10 +763,7 @@ class SexpRenderer {
|
||||
buf.append(tab)
|
||||
buf.append("(moduleType)")
|
||||
}
|
||||
is StringConstantType -> {
|
||||
buf.append(tab)
|
||||
buf.append("(stringConstantType)")
|
||||
}
|
||||
is StringConstantType -> renderStringConstantType(type)
|
||||
is DeclaredType -> renderDeclaredType(type)
|
||||
is ParenthesizedType -> renderParenthesizedType(type)
|
||||
is NullableType -> renderNullableType(type)
|
||||
@@ -750,6 +773,16 @@ class SexpRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
fun renderStringConstantType(type: StringConstantType) {
|
||||
buf.append(tab)
|
||||
buf.append("(stringConstantType")
|
||||
val oldTab = increaseTab()
|
||||
buf.append('\n')
|
||||
renderStringConstant(type.str)
|
||||
buf.append(')')
|
||||
tab = oldTab
|
||||
}
|
||||
|
||||
fun renderDeclaredType(type: DeclaredType) {
|
||||
buf.append(tab)
|
||||
buf.append("(declaredType")
|
||||
@@ -778,7 +811,7 @@ class SexpRenderer {
|
||||
|
||||
fun renderParenthesizedType(type: ParenthesizedType) {
|
||||
buf.append(tab)
|
||||
buf.append("(parenthesisedType")
|
||||
buf.append("(parenthesizedType")
|
||||
val oldTab = increaseTab()
|
||||
buf.append('\n')
|
||||
renderType(type.type)
|
||||
@@ -903,12 +936,11 @@ class SexpRenderer {
|
||||
buf.append(tab)
|
||||
buf.append("(objectMethod")
|
||||
val oldTab = increaseTab()
|
||||
buf.append('\n')
|
||||
for (mod in method.modifiers) {
|
||||
buf.append('\n')
|
||||
renderModifier(mod)
|
||||
}
|
||||
buf.append('\n')
|
||||
buf.append('\n').append(tab)
|
||||
buf.append("(identifier)")
|
||||
val tparList = method.typeParameterList
|
||||
if (tparList !== null) {
|
||||
@@ -1012,19 +1044,20 @@ class SexpRenderer {
|
||||
|
||||
fun renderTypeParameterList(typeParameterList: TypeParameterList) {
|
||||
buf.append(tab)
|
||||
buf.append("(TypeParameterList\n")
|
||||
buf.append("(typeParameterList")
|
||||
val oldTab = increaseTab()
|
||||
for (tpar in typeParameterList.parameters) {
|
||||
buf.append('\n')
|
||||
renderTypeParameter(tpar)
|
||||
}
|
||||
buf.append(')')
|
||||
tab = oldTab
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun renderTypeParameter(ignored: TypeParameter?) {
|
||||
buf.append(tab)
|
||||
buf.append("(TypeParameter\n")
|
||||
buf.append("(typeParameter\n")
|
||||
val oldTab = increaseTab()
|
||||
buf.append(tab)
|
||||
buf.append("(identifier))")
|
||||
|
||||
Reference in New Issue
Block a user