forked from INRIA/spoon
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: print minimal amount of round brackets in sniper mode (INRIA#3823)
- Loading branch information
1 parent
c121af4
commit fb8ee5f
Showing
7 changed files
with
362 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
src/main/java/spoon/reflect/visitor/RoundBracketAnalyzer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/** | ||
* SPDX-License-Identifier: (MIT OR CECILL-C) | ||
* | ||
* Copyright (C) 2006-2019 INRIA and contributors | ||
* | ||
* 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. | ||
*/ | ||
package spoon.reflect.visitor; | ||
|
||
import spoon.reflect.code.CtBinaryOperator; | ||
import spoon.reflect.code.CtExpression; | ||
import spoon.reflect.code.CtUnaryOperator; | ||
import spoon.reflect.declaration.CtElement; | ||
|
||
/** | ||
* Class for determining whether or not an expression requires round brackets in order to preserve | ||
* AST structure (and consequently semantics). | ||
*/ | ||
class RoundBracketAnalyzer { | ||
|
||
enum EncloseInRoundBrackets { | ||
YES, NO, UNKNOWN; | ||
} | ||
|
||
private RoundBracketAnalyzer() { | ||
} | ||
|
||
/** | ||
* @param expr A unary or binary expr. | ||
* @return true if the expr should be enclosed in round brackets. | ||
*/ | ||
static EncloseInRoundBrackets requiresRoundBrackets(CtExpression<?> expr) { | ||
return isNestedOperator(expr) | ||
? nestedOperatorRequiresRoundBrackets(expr) | ||
: EncloseInRoundBrackets.UNKNOWN; | ||
} | ||
|
||
/** | ||
* Assuming that operator is a nested operator (i.e. both operator and its parent are | ||
* {@link CtUnaryOperator} or {@link CtBinaryOperator}), determine whether or not it must be | ||
* enclosed in round brackets. | ||
* | ||
* Given an element <code>e</code> with a parent <code>p</code>, we must parenthesize | ||
* <code>e</code> if any of the following are true. | ||
* | ||
* <ul> | ||
* <li>The parent p is a unary operator</li> | ||
* <li>The parent p is a binary operator, and <code>precedence(p) > precedence(e></code></li> | ||
* <li>The parent p is a binary operator, <code>precedence(p) == precedence(e)</code>, | ||
* e appears as the X-hand-side operand of p, and e's operator is Y-associative, where | ||
* <code>X != Y</code></li> | ||
* </ul> | ||
* | ||
* Note that the final rule is necessary to preserve syntactical structure, but it is not | ||
* required for preserving semantics. | ||
* | ||
* @param nestedOperator A nested operator. | ||
* @return Whether or not to enclose the nested operator in round brackets. | ||
*/ | ||
private static EncloseInRoundBrackets nestedOperatorRequiresRoundBrackets(CtExpression<?> nestedOperator) { | ||
if (nestedOperator.getParent() instanceof CtUnaryOperator) { | ||
return EncloseInRoundBrackets.YES; | ||
} | ||
|
||
OperatorHelper.OperatorAssociativity associativity = getOperatorAssociativity(nestedOperator); | ||
OperatorHelper.OperatorAssociativity positionInParent = getPositionInParent(nestedOperator); | ||
|
||
int parentPrecedence = getOperatorPrecedence(nestedOperator.getParent()); | ||
int precedence = getOperatorPrecedence(nestedOperator); | ||
return precedence < parentPrecedence | ||
|| (precedence == parentPrecedence && associativity != positionInParent) | ||
? EncloseInRoundBrackets.YES | ||
: EncloseInRoundBrackets.NO; | ||
} | ||
|
||
private static boolean isNestedOperator(CtElement e) { | ||
return e.isParentInitialized() && isOperator(e) && isOperator(e.getParent()); | ||
} | ||
|
||
private static boolean isOperator(CtElement e) { | ||
return e instanceof CtBinaryOperator || e instanceof CtUnaryOperator; | ||
} | ||
|
||
private static int getOperatorPrecedence(CtElement e) { | ||
if (e instanceof CtBinaryOperator) { | ||
return OperatorHelper.getOperatorPrecedence(((CtBinaryOperator<?>) e).getKind()); | ||
} else if (e instanceof CtUnaryOperator) { | ||
return OperatorHelper.getOperatorPrecedence(((CtUnaryOperator<?>) e).getKind()); | ||
} else { | ||
return 0; | ||
} | ||
} | ||
|
||
private static OperatorHelper.OperatorAssociativity getOperatorAssociativity(CtElement e) { | ||
if (e instanceof CtBinaryOperator) { | ||
return OperatorHelper.getOperatorAssociativity(((CtBinaryOperator<?>) e).getKind()); | ||
} else if (e instanceof CtUnaryOperator) { | ||
return OperatorHelper.getOperatorAssociativity(((CtUnaryOperator<?>) e).getKind()); | ||
} else { | ||
return OperatorHelper.OperatorAssociativity.NONE; | ||
} | ||
} | ||
|
||
private static OperatorHelper.OperatorAssociativity getPositionInParent(CtElement e) { | ||
CtElement parent = e.getParent(); | ||
if (parent instanceof CtBinaryOperator) { | ||
return ((CtBinaryOperator<?>) parent).getLeftHandOperand() == e | ||
? OperatorHelper.OperatorAssociativity.LEFT | ||
: OperatorHelper.OperatorAssociativity.RIGHT; | ||
} else { | ||
return OperatorHelper.OperatorAssociativity.NONE; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
src/test/java/spoon/reflect/visitor/DefaultJavaPrettyPrinterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package spoon.reflect.visitor; | ||
|
||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.ValueSource; | ||
import spoon.Launcher; | ||
import spoon.reflect.code.CtExpression; | ||
import spoon.reflect.code.CtStatement; | ||
|
||
import static org.hamcrest.CoreMatchers.equalTo; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
|
||
public class DefaultJavaPrettyPrinterTest { | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = { | ||
"1 + 2 + 3", | ||
"1 + (2 + 3)", | ||
"1 + 2 + -3", | ||
"1 + 2 + -(2 + 3)", | ||
"\"Sum: \" + (1 + 2)", | ||
"\"Sum: \" + 1 + 2", | ||
"-(1 + 2 + 3)", | ||
"true || true && false", | ||
"(true || false) && false", | ||
"1 | 2 | 3", | ||
"1 | (2 | 3)", | ||
"1 | 2 & 3", | ||
"(1 | 2) & 3", | ||
"1 | 2 ^ 3", | ||
"(1 | 2) ^ 3" | ||
}) | ||
public void testParenOptimizationCorrectlyPrintsParenthesesForExpressions(String rawExpression) { | ||
// contract: When input expressions are minimally parenthesized, pretty-printed output | ||
// should match the input | ||
CtExpression<?> expr = createLauncherWithOptimizeParenthesesPrinter() | ||
.getFactory().createCodeSnippetExpression(rawExpression).compile(); | ||
assertThat(expr.toString(), equalTo(rawExpression)); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = { | ||
"int sum = 1 + 2 + 3", | ||
"java.lang.String s = \"Sum: \" + (1 + 2)", | ||
"java.lang.String s = \"Sum: \" + 1 + 2" | ||
}) | ||
public void testParenOptimizationCorrectlyPrintsParenthesesForStatements(String rawStatement) { | ||
// contract: When input expressions as part of statements are minimally parenthesized, | ||
// pretty-printed output should match the input | ||
CtStatement statement = createLauncherWithOptimizeParenthesesPrinter() | ||
.getFactory().createCodeSnippetStatement(rawStatement).compile(); | ||
assertThat(statement.toString(), equalTo(rawStatement)); | ||
} | ||
|
||
private static Launcher createLauncherWithOptimizeParenthesesPrinter() { | ||
Launcher launcher = new Launcher(); | ||
launcher.getEnvironment().setPrettyPrinterCreator(() -> { | ||
DefaultJavaPrettyPrinter printer = new DefaultJavaPrettyPrinter(launcher.getEnvironment()); | ||
printer.setMinimizeRoundBrackets(true); | ||
return printer; | ||
}); | ||
return launcher; | ||
} | ||
} |
Oops, something went wrong.