Skip to content

Commit fb8ee5f

Browse files
slarsewoutersmeenk
authored andcommitted
feat: print minimal amount of round brackets in sniper mode (INRIA#3823)
1 parent c121af4 commit fb8ee5f

File tree

7 files changed

+362
-1
lines changed

7 files changed

+362
-1
lines changed

spoon-pom/pom.xml

+7-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@
5454
<version>5.7.1</version>
5555
<scope>test</scope>
5656
</dependency>
57-
57+
<dependency>
58+
<groupId>org.junit.jupiter</groupId>
59+
<artifactId>junit-jupiter-params</artifactId>
60+
<version>5.7.1</version>
61+
<scope>test</scope>
62+
</dependency>
63+
5864
<dependency>
5965
<!-- must come after junit5 deps, otherwise it overrides the junit version -->
6066
<!-- must be here in the parent POM, because the parent always comes last, see https://stackoverflow.com/questions/28999057/order-of-inherited-dependencies-in-maven-3 -->

src/main/java/spoon/reflect/visitor/DefaultJavaPrettyPrinter.java

+38
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ public class DefaultJavaPrettyPrinter implements CtVisitor, PrettyPrinter {
217217
*/
218218
protected boolean ignoreImplicit = true;
219219

220+
/**
221+
* EXPERIMENTAL: If true, the printer will attempt to print a minimal set of round brackets in
222+
* expressions while preserving the syntactical structure of the AST.
223+
*/
224+
private boolean minimizeRoundBrackets = false;
225+
220226
public boolean inlineElseIf = true;
221227

222228
/**
@@ -391,6 +397,13 @@ private boolean shouldSetBracket(CtExpression<?> e) {
391397
return true;
392398
}
393399
try {
400+
if (isMinimizeRoundBrackets()) {
401+
RoundBracketAnalyzer.EncloseInRoundBrackets requiresBrackets =
402+
RoundBracketAnalyzer.requiresRoundBrackets(e);
403+
if (requiresBrackets != RoundBracketAnalyzer.EncloseInRoundBrackets.UNKNOWN) {
404+
return requiresBrackets == RoundBracketAnalyzer.EncloseInRoundBrackets.YES;
405+
}
406+
}
394407
if ((e.getParent() instanceof CtBinaryOperator) || (e.getParent() instanceof CtUnaryOperator)) {
395408
return (e instanceof CtAssignment) || (e instanceof CtConditional) || (e instanceof CtUnaryOperator) || e instanceof CtBinaryOperator;
396409
}
@@ -2130,4 +2143,29 @@ public void visitCtYieldStatement(CtYieldStatement statement) {
21302143
scan(statement.getExpression());
21312144
exitCtStatement(statement);
21322145
}
2146+
2147+
/**
2148+
* @return true if the printer is minimizing the amount of round brackets in expressions
2149+
*/
2150+
protected boolean isMinimizeRoundBrackets() {
2151+
return minimizeRoundBrackets;
2152+
}
2153+
2154+
/**
2155+
* When set to true, this activates round bracket minimization for expressions. This means that
2156+
* the printer will attempt to only write round brackets strictly necessary for preserving
2157+
* syntactical structure (and by extension, semantics).
2158+
*
2159+
* As an example, the expression <code>1 + 2 + 3 + 4</code> is written as
2160+
* <code>((1 + 2) + 3) + 4</code> without round bracket minimization, but entirely without
2161+
* parentheses when minimization is enabled. However, an expression <code>1 + 2 + (3 + 4)</code>
2162+
* is still written as <code>1 + 2 + (3 + 4)</code> to preserve syntactical structure, even though
2163+
* the brackets are semantically redundant.
2164+
*
2165+
* @param minimizeRoundBrackets set whether or not to minimize round brackets in expressions
2166+
*/
2167+
protected void setMinimizeRoundBrackets(boolean minimizeRoundBrackets) {
2168+
this.minimizeRoundBrackets = minimizeRoundBrackets;
2169+
}
2170+
21332171
}

src/main/java/spoon/reflect/visitor/OperatorHelper.java

+118
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
*/
1717
class OperatorHelper {
1818

19+
public enum OperatorAssociativity {
20+
LEFT, RIGHT, NONE
21+
}
22+
1923
private OperatorHelper() {
2024
}
2125

@@ -101,4 +105,118 @@ public static String getOperatorText(BinaryOperatorKind o) {
101105
throw new SpoonException("Unsupported operator " + o.name());
102106
}
103107
}
108+
109+
/**
110+
* Get the precedence of a binary operator as defined by
111+
* https://introcs.cs.princeton.edu/java/11precedence/
112+
*
113+
* @param o A binary operator kind.
114+
* @return The precedence of the given operator.
115+
*/
116+
public static int getOperatorPrecedence(BinaryOperatorKind o) {
117+
switch (o) {
118+
case OR: // ||
119+
return 3;
120+
case AND: // &&
121+
return 4;
122+
case BITOR: // |
123+
return 5;
124+
case BITXOR: // ^
125+
return 6;
126+
case BITAND: // &
127+
return 7;
128+
case EQ: // ==
129+
case NE: // !=
130+
return 8;
131+
case LT: // <
132+
case GT: // >
133+
case LE: // <=
134+
case GE: // >=
135+
case INSTANCEOF:
136+
return 9;
137+
case SL: // <<
138+
case SR: // >>
139+
case USR: // >>>
140+
return 10;
141+
case PLUS: // +
142+
case MINUS: // -
143+
return 11;
144+
case MUL: // *
145+
case DIV: // /
146+
case MOD: // %
147+
return 12;
148+
default:
149+
throw new SpoonException("Unsupported operator " + o.name());
150+
}
151+
}
152+
153+
/**
154+
* Get the precedence of a unary operator as defined by
155+
* https://introcs.cs.princeton.edu/java/11precedence/
156+
*
157+
* @param o A unary operator kind.
158+
* @return The precedence of the given operator.
159+
*/
160+
public static int getOperatorPrecedence(UnaryOperatorKind o) {
161+
switch (o) {
162+
case POS:
163+
case NEG:
164+
case NOT:
165+
case COMPL:
166+
case PREINC:
167+
case PREDEC:
168+
return 14;
169+
case POSTINC:
170+
case POSTDEC:
171+
return 15;
172+
default:
173+
throw new SpoonException("Unsupported operator " + o.name());
174+
}
175+
}
176+
177+
/**
178+
* Get the associativity of a binary operator as defined by
179+
* https://introcs.cs.princeton.edu/java/11precedence/
180+
*
181+
* All binary operators are left-associative in Java, except for the relational operators that
182+
* have no associativity (i.e. you can't chain them).
183+
*
184+
* There's an exception: the ternary operator ?: is right-associative, but that's not an
185+
* operator kind in Spoon so we don't deal with it.
186+
*
187+
* @param o A binary operator kind.
188+
* @return The associativity of the operator.
189+
*/
190+
public static OperatorAssociativity getOperatorAssociativity(BinaryOperatorKind o) {
191+
switch (o) {
192+
case LT: // <
193+
case GT: // >
194+
case LE: // <=
195+
case GE: // >=
196+
case INSTANCEOF:
197+
return OperatorAssociativity.NONE;
198+
default:
199+
return OperatorAssociativity.LEFT;
200+
}
201+
}
202+
203+
/**
204+
* Get the associativity of a unary operator, as defined by
205+
* https://introcs.cs.princeton.edu/java/11precedence/
206+
*
207+
* All unary operators are right-associative, except for post increment and decrement, which
208+
* are not associative.
209+
*
210+
* @param o A unary operator kind.
211+
* @return The associativity of the operator.
212+
*/
213+
public static OperatorAssociativity getOperatorAssociativity(UnaryOperatorKind o) {
214+
switch (o) {
215+
case POSTINC:
216+
case POSTDEC:
217+
return OperatorAssociativity.NONE;
218+
default:
219+
return OperatorAssociativity.RIGHT;
220+
}
221+
}
104222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* SPDX-License-Identifier: (MIT OR CECILL-C)
3+
*
4+
* Copyright (C) 2006-2019 INRIA and contributors
5+
*
6+
* Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) of the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon.
7+
*/
8+
package spoon.reflect.visitor;
9+
10+
import spoon.reflect.code.CtBinaryOperator;
11+
import spoon.reflect.code.CtExpression;
12+
import spoon.reflect.code.CtUnaryOperator;
13+
import spoon.reflect.declaration.CtElement;
14+
15+
/**
16+
* Class for determining whether or not an expression requires round brackets in order to preserve
17+
* AST structure (and consequently semantics).
18+
*/
19+
class RoundBracketAnalyzer {
20+
21+
enum EncloseInRoundBrackets {
22+
YES, NO, UNKNOWN;
23+
}
24+
25+
private RoundBracketAnalyzer() {
26+
}
27+
28+
/**
29+
* @param expr A unary or binary expr.
30+
* @return true if the expr should be enclosed in round brackets.
31+
*/
32+
static EncloseInRoundBrackets requiresRoundBrackets(CtExpression<?> expr) {
33+
return isNestedOperator(expr)
34+
? nestedOperatorRequiresRoundBrackets(expr)
35+
: EncloseInRoundBrackets.UNKNOWN;
36+
}
37+
38+
/**
39+
* Assuming that operator is a nested operator (i.e. both operator and its parent are
40+
* {@link CtUnaryOperator} or {@link CtBinaryOperator}), determine whether or not it must be
41+
* enclosed in round brackets.
42+
*
43+
* Given an element <code>e</code> with a parent <code>p</code>, we must parenthesize
44+
* <code>e</code> if any of the following are true.
45+
*
46+
* <ul>
47+
* <li>The parent p is a unary operator</li>
48+
* <li>The parent p is a binary operator, and <code>precedence(p) > precedence(e></code></li>
49+
* <li>The parent p is a binary operator, <code>precedence(p) == precedence(e)</code>,
50+
* e appears as the X-hand-side operand of p, and e's operator is Y-associative, where
51+
* <code>X != Y</code></li>
52+
* </ul>
53+
*
54+
* Note that the final rule is necessary to preserve syntactical structure, but it is not
55+
* required for preserving semantics.
56+
*
57+
* @param nestedOperator A nested operator.
58+
* @return Whether or not to enclose the nested operator in round brackets.
59+
*/
60+
private static EncloseInRoundBrackets nestedOperatorRequiresRoundBrackets(CtExpression<?> nestedOperator) {
61+
if (nestedOperator.getParent() instanceof CtUnaryOperator) {
62+
return EncloseInRoundBrackets.YES;
63+
}
64+
65+
OperatorHelper.OperatorAssociativity associativity = getOperatorAssociativity(nestedOperator);
66+
OperatorHelper.OperatorAssociativity positionInParent = getPositionInParent(nestedOperator);
67+
68+
int parentPrecedence = getOperatorPrecedence(nestedOperator.getParent());
69+
int precedence = getOperatorPrecedence(nestedOperator);
70+
return precedence < parentPrecedence
71+
|| (precedence == parentPrecedence && associativity != positionInParent)
72+
? EncloseInRoundBrackets.YES
73+
: EncloseInRoundBrackets.NO;
74+
}
75+
76+
private static boolean isNestedOperator(CtElement e) {
77+
return e.isParentInitialized() && isOperator(e) && isOperator(e.getParent());
78+
}
79+
80+
private static boolean isOperator(CtElement e) {
81+
return e instanceof CtBinaryOperator || e instanceof CtUnaryOperator;
82+
}
83+
84+
private static int getOperatorPrecedence(CtElement e) {
85+
if (e instanceof CtBinaryOperator) {
86+
return OperatorHelper.getOperatorPrecedence(((CtBinaryOperator<?>) e).getKind());
87+
} else if (e instanceof CtUnaryOperator) {
88+
return OperatorHelper.getOperatorPrecedence(((CtUnaryOperator<?>) e).getKind());
89+
} else {
90+
return 0;
91+
}
92+
}
93+
94+
private static OperatorHelper.OperatorAssociativity getOperatorAssociativity(CtElement e) {
95+
if (e instanceof CtBinaryOperator) {
96+
return OperatorHelper.getOperatorAssociativity(((CtBinaryOperator<?>) e).getKind());
97+
} else if (e instanceof CtUnaryOperator) {
98+
return OperatorHelper.getOperatorAssociativity(((CtUnaryOperator<?>) e).getKind());
99+
} else {
100+
return OperatorHelper.OperatorAssociativity.NONE;
101+
}
102+
}
103+
104+
private static OperatorHelper.OperatorAssociativity getPositionInParent(CtElement e) {
105+
CtElement parent = e.getParent();
106+
if (parent instanceof CtBinaryOperator) {
107+
return ((CtBinaryOperator<?>) parent).getLeftHandOperand() == e
108+
? OperatorHelper.OperatorAssociativity.LEFT
109+
: OperatorHelper.OperatorAssociativity.RIGHT;
110+
} else {
111+
return OperatorHelper.OperatorAssociativity.NONE;
112+
}
113+
}
114+
}

src/main/java/spoon/support/sniper/SniperJavaPrettyPrinter.java

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ public SniperJavaPrettyPrinter(Environment env) {
8282

8383
// newly added elements are not fully qualified
8484
this.setIgnoreImplicit(false);
85+
86+
// don't print redundant parentheses
87+
this.setMinimizeRoundBrackets(true);
8588
}
8689

8790
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package spoon.reflect.visitor;
2+
3+
import org.junit.jupiter.params.ParameterizedTest;
4+
import org.junit.jupiter.params.provider.ValueSource;
5+
import spoon.Launcher;
6+
import spoon.reflect.code.CtExpression;
7+
import spoon.reflect.code.CtStatement;
8+
9+
import static org.hamcrest.CoreMatchers.equalTo;
10+
import static org.hamcrest.MatcherAssert.assertThat;
11+
12+
public class DefaultJavaPrettyPrinterTest {
13+
14+
@ParameterizedTest
15+
@ValueSource(strings = {
16+
"1 + 2 + 3",
17+
"1 + (2 + 3)",
18+
"1 + 2 + -3",
19+
"1 + 2 + -(2 + 3)",
20+
"\"Sum: \" + (1 + 2)",
21+
"\"Sum: \" + 1 + 2",
22+
"-(1 + 2 + 3)",
23+
"true || true && false",
24+
"(true || false) && false",
25+
"1 | 2 | 3",
26+
"1 | (2 | 3)",
27+
"1 | 2 & 3",
28+
"(1 | 2) & 3",
29+
"1 | 2 ^ 3",
30+
"(1 | 2) ^ 3"
31+
})
32+
public void testParenOptimizationCorrectlyPrintsParenthesesForExpressions(String rawExpression) {
33+
// contract: When input expressions are minimally parenthesized, pretty-printed output
34+
// should match the input
35+
CtExpression<?> expr = createLauncherWithOptimizeParenthesesPrinter()
36+
.getFactory().createCodeSnippetExpression(rawExpression).compile();
37+
assertThat(expr.toString(), equalTo(rawExpression));
38+
}
39+
40+
@ParameterizedTest
41+
@ValueSource(strings = {
42+
"int sum = 1 + 2 + 3",
43+
"java.lang.String s = \"Sum: \" + (1 + 2)",
44+
"java.lang.String s = \"Sum: \" + 1 + 2"
45+
})
46+
public void testParenOptimizationCorrectlyPrintsParenthesesForStatements(String rawStatement) {
47+
// contract: When input expressions as part of statements are minimally parenthesized,
48+
// pretty-printed output should match the input
49+
CtStatement statement = createLauncherWithOptimizeParenthesesPrinter()
50+
.getFactory().createCodeSnippetStatement(rawStatement).compile();
51+
assertThat(statement.toString(), equalTo(rawStatement));
52+
}
53+
54+
private static Launcher createLauncherWithOptimizeParenthesesPrinter() {
55+
Launcher launcher = new Launcher();
56+
launcher.getEnvironment().setPrettyPrinterCreator(() -> {
57+
DefaultJavaPrettyPrinter printer = new DefaultJavaPrettyPrinter(launcher.getEnvironment());
58+
printer.setMinimizeRoundBrackets(true);
59+
return printer;
60+
});
61+
return launcher;
62+
}
63+
}

0 commit comments

Comments
 (0)