-
-
Notifications
You must be signed in to change notification settings - Fork 357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: print minimal amount of round brackets in sniper mode #3823
Merged
monperrus
merged 30 commits into
INRIA:master
from
slarse:issue/3809-print-minimal-brackets-for-uop-and-binop
Mar 10, 2021
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
a438c09
Add naive implementation that considers only precedence
slarse 606c90a
Preserve AST structure
slarse f9038e7
Add test to verify that unary operator parentheses are kept
slarse 5680169
Refactor duplicated test logic into parameterized test
slarse a9bdf76
Fix parenthesis optimization associativity rule when parent has highe…
slarse 6dc3bed
Add another expression
slarse e2115a3
Add parameterized test for testing statements
slarse 89ff049
Add support for optimizing parentheses of unary operators
slarse f1aa4d4
Positiong overloads next to each other
slarse 6f91d65
Add test expressions for bitwise operators
slarse 848402a
Add explicit contracts to test cases
slarse 43f7d95
Refactor test cases
slarse bf84230
Fix style issues
slarse 9d00a64
Refactor
slarse 41db039
Remove unused imports
slarse 0ff1a45
Clarify intent by removing negation
slarse f831167
Adjust note on optimizeParentheses field
slarse 03de60d
Merge branch 'master' into issue/3809-print-minimal-brackets-for-uop-…
slarse ea40045
Refactor parenthesis calculation into separate class
slarse 636624e
Activate parenthesis optimization for sniper printer
slarse ad087f8
Document parenthesis optimization
slarse 2e2c406
Reformat ParenOptimizer with tabs
slarse df6bcf1
Add private constructor to ParenOptimizer
slarse e584ae5
Revert redundant whitespace changes
slarse b664773
Remove redundant blank line
slarse 2e83211
Add license header
slarse e5c82d5
Fix broken test
slarse db058e1
Revise terminology: parenthesis -> round bracket, optimize -> minimize
slarse 73338f7
Fix indentation
slarse a6a43e9
Remove funky header formatting inserted by IntelliJ
slarse File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This dependency is necessary for using the
@ParameterizedTest
annotation.