Skip to content
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

Adding feature for removing unnecessary lambda expression block . #167

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions java/piranha/src/main/java/com/uber/piranha/XPFlagCleaner.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
Expand Down Expand Up @@ -129,6 +130,7 @@ public class XPFlagCleaner extends BugChecker
BugChecker.ReturnTreeMatcher,
BugChecker.UnaryTreeMatcher,
BugChecker.VariableTreeMatcher,
BugChecker.LambdaExpressionTreeMatcher,
BugChecker.MethodTreeMatcher {

/**
Expand Down Expand Up @@ -1038,6 +1040,36 @@ public Description matchVariable(VariableTree tree, VisitorState state) {
return Description.NO_MATCH;
}

// This method removes unnecessary blocks from lambda expression bodies.
// This scenario is possible in case of single return statements and expression statements
// in the lambda body after Piranha removes the code related to stale flags.
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState visitorState) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is matching on every lambda expression of the original AST, in parallel with the traversal you are using to rewrite XP flag using code. This will result in two issues:

a) This will not actually simplify expressions after Piranha removes the code related to stale flags, since that happens later in the traversal (deeper inside the AST) than this matcher. And also since Piranha doesn't rewrite the AST in place (for good reasons, given the EP model) but rather generates a fix suggestion to be applied after compilation and analysis by the checker.

b) This will switch all single-statement lambdas present in the original code to use the expression body style vs the block body style. This is not something we want Piranha to be doing. In general, this is not the tool to do code changes that are entirely unrelated to removing stale flags.

I am not sure that the refactoring you want to implement can be done easily. You would either need to match the "lambda containing an if" pattern here and duplicate/refactor much of the matchIf logic, or have secondary traversal over the already rewritten code, but this is hard to do inside the Piranha checker itself and would require some integration work to run two rewriters (it would also need some logic to detect that the code being "simplified" was already touched by Piranha and is not an unrelated lambda expression).

This is an instance of the "deep clean" issue with Piranha Java.

Tree lambdaBody = tree.getBody();
if (lambdaBody instanceof BlockTree) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are more fundamental problems (see above), but you want LambdaExpressionTree.getBodyKind()

BlockTree bt = (BlockTree) lambdaBody;
// Block should contain only one statement
if (bt.getStatements().size() == 1) {
StatementTree singleStmt = bt.getStatements().get(0);
ExpressionTree expr = null;
if (singleStmt instanceof ReturnTree) {
ReturnTree stmt = (ReturnTree) singleStmt;
expr = stmt.getExpression();
}
if (singleStmt instanceof ExpressionStatementTree) {
ExpressionStatementTree expStmt = (ExpressionStatementTree) singleStmt;
expr = expStmt.getExpression();
}
if (expr != null) {
return buildDescription(tree)
.addFix(SuggestedFix.replace(bt, visitorState.getSourceForNode(expr)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be safer to build a new lambda expression with expr as the body and replace tree (but again, this is moot, since this won't do the rewrite you want).

.build();
}
}
}
return buildDescription(tree).build();
}

/** Represents the character at the end of an enum constant */
enum EnumEnding {
// Ignore the result of the trailing enum character, whether we know what it is or not
Expand Down
217 changes: 86 additions & 131 deletions java/piranha/src/test/java/com/uber/piranha/CorePiranhaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,92 @@ public void testIgnoresPrefixMatchFlag() throws IOException {
.doTest();
}

// Tests for cleanup of unnecessary blocks in the body of a lambda expression.
@Test
public void testRemoveUnnecessaryBlockLambda() throws IOException {
ErrorProneFlags.Builder b = ErrorProneFlags.builder();
b.putFlag("Piranha:FlagName", "STALE_FLAG");
b.putFlag("Piranha:IsTreated", "false");
b.putFlag("Piranha:Config", "config/properties.json");

BugCheckerRefactoringTestHelper bcr =
BugCheckerRefactoringTestHelper.newInstance(new XPFlagCleaner(b.build()), getClass());

bcr = bcr.setArgs("-d", temporaryFolder.getRoot().getAbsolutePath());
bcr = PiranhaTestingHelpers.addHelperClasses(bcr);

bcr.addInputLines(
"TestRemoveBlocks.java",
"package com.uber.piranha;",
"import java.util.function.Consumer;",
"import java.util.function.Supplier;",
"class TestRemoveBlocks {",
" private XPTest experimentation;",
" public void testMethod(){",
" Consumer<String> someConsumer = (s) -> {",
" if(experimentation.isToggleDisabled(\"STALE_FLAG\")){",
" System.out.println(s);",
" } ",
" else {",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably doesn't matter for a test, but this is a weird way to format the code vs } else {. Similar in the cases below.

" System.out.println(s + \"123\");",
" }",
" };",
" Supplier<String> someSupplier = () -> {",
" if(experimentation.isToggleDisabled(\"STALE_FLAG\")){",
" return \"ABC\";",
" } ",
" else {",
" return \"123\";",
" }",
" };",
" Supplier<String> someSupplier1 = () -> {",
" if(experimentation.isToggleDisabled(\"STALE_FLAG\")){",
" System.out.println(\"ABC\");",
" return \"ABC\";",
" } ",
" else {",
" System.out.println(\"123\");",
" return \"123\";",
" }",
" };",
" Consumer<String> someConsumer1 = (s) -> {",
" if(experimentation.isToggleDisabled(\"STALE_FLAG\")){",
" System.out.println(s);",
" } ",
" else {",
" System.out.println(s + \"123\");",
" }",
" System.out.println(\"No match\");",
" };",
" }",
"}")
.addOutputLines(
"TestRemoveBlocks.java",
"package com.uber.piranha;",
"import java.util.function.Consumer;",
"import java.util.function.Supplier;",
"class TestRemoveBlocks {",
" private XPTest experimentation;",
" public void testMethod(){",
" Consumer<String> someConsumer = (s) -> {",
" System.out.println(s);",
" };",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. Aren't these blocks exactly what you are trying to remove? Shouldn't the rewrite here be:

Consumer<String> someConsumer = s -> System.out.println(s);
Supplier<String> someSupplier = () -> \"ABC\";
[...]

?

" Supplier<String> someSupplier = () -> {",
" return \"ABC\";",
" };",
" Supplier<String> someSupplier1 = () -> {",
" System.out.println(\"ABC\");",
" return \"ABC\";",
" };",
" Consumer<String> someConsumer1 = (s) -> {",
" System.out.println(s);",
" System.out.println(\"No match\");",
" };",
" }",
"}")
.doTest();
}

@Test
public void testRemoveSpecificAPIpatternsMockito() throws IOException {

Expand Down Expand Up @@ -954,137 +1040,6 @@ public void testRemoveSpecificAPIPatternsInstanceMethod() throws IOException {
.doTest();
}

@Test
public void testRemoveSpecificAPIpatterns() throws IOException {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my previous PR, I had split this test case into smaller test case. But I had not deleted this test case.


ErrorProneFlags.Builder b = ErrorProneFlags.builder();
b.putFlag("Piranha:FlagName", "STALE_FLAG");
b.putFlag("Piranha:IsTreated", "false");
b.putFlag("Piranha:Config", "config/properties.json");

BugCheckerRefactoringTestHelper bcr =
BugCheckerRefactoringTestHelper.newInstance(new XPFlagCleaner(b.build()), getClass());

bcr = bcr.setArgs("-d", temporaryFolder.getRoot().getAbsolutePath());
bcr = PiranhaTestingHelpers.addHelperClasses(bcr);
bcr.addInputLines(
"XPFlagCleanerSinglePositiveCase.java",
"package com.uber.piranha;",
"import static org.mockito.Mockito.when;",
"import org.mockito.invocation.InvocationOnMock;",
"import org.mockito.stubbing.Answer;",
"import org.mockito.stubbing.OngoingStubbing;",
"import org.easymock.EasyMock;",
"import static org.junit.Assert.assertFalse;",
"import static org.junit.Assert.assertTrue;",
"class MockitoTest {",
" private XPTest experimentation;",
" public void evaluate() {",
"// Mockito Test Scenarios",
" Answer ans = new Answer<Integer>() {\n"
+ " public Integer answer(InvocationOnMock invocation) throws Throwable {\n"
+ " return (Integer) invocation.getArguments()[0];\n"
+ " }};",
" when(experimentation.isToggleDisabled(\"STALE_FLAG\")).thenReturn(false);",
" when(experimentation.isToggleEnabled(\"STALE_FLAG\")).thenThrow(new RuntimeException());",
" when(experimentation.isToggleDisabled(\"STALE_FLAG\")).thenCallRealMethod();",
" when(experimentation.isToggleEnabled(\"STALE_FLAG\")).then(ans);",
" boolean b1 = someWrapper(when(experimentation.isToggleDisabled(\"STALE_FLAG\")).thenCallRealMethod());",
" boolean b2 = someWrapper(when(experimentation.isToggleDisabled(\"OTHER_FLAG\")).thenCallRealMethod());",
" someWhenWrapper(when(experimentation.isToggleDisabled(\"STALE_FLAG\"))).thenCallRealMethod();",
" someWhenWrapper(when(experimentation.isToggleDisabled(\"OTHER_FLAG\"))).thenCallRealMethod();",
" when(experimentation.isToggleEnabled(\"OTHER_FLAG\")).thenReturn(false);",
" when(experimentation.isToggleEnabled(\"OTHER_FLAG\")).thenThrow(new RuntimeException());",
" when(experimentation.isToggleDisabled(\"OTHER_FLAG\")).thenCallRealMethod();",
" when(experimentation.isToggleEnabled(\"OTHER_FLAG\")).then(ans);",
" when(foobar()).thenReturn(false);",
" when(foobar()).thenAnswer(ans);",
"// Easymock Test scenarios",
" EasyMock.expect(experimentation.isToggleDisabled(\"STALE_FLAG\")).andReturn(true).anyTimes();",
" EasyMock.expect(experimentation.isToggleEnabled(\"STALE_FLAG\")).andReturn(true).times(5);",
" EasyMock.expect(experimentation.isToggleDisabled(\"STALE_FLAG\")).andReturn(true).times(5, 6);",
" EasyMock.expect(experimentation.isToggleDisabled(\"STALE_FLAG\")).andReturn(true).once();",
" EasyMock.expect(experimentation.isToggleEnabled(\"STALE_FLAG\")).andReturn(true).atLeastOnce();",
" EasyMock.expect(experimentation.isToggleEnabled(\"STALE_FLAG\")).andReturn(true).anyTimes();",
" EasyMock.expect(experimentation.isToggleEnabled(\"STALE_FLAG\")).andReturn(true);",
" EasyMock.expect(experimentation.isToggleEnabled(\"STALE_FLAG\")).asStub();",
" EasyMock.expect(experimentation.isToggleEnabled(\"OTHER_FLAG\")).andReturn(true).anyTimes();",
" EasyMock.expect(foobar()).andReturn(true).times(5);",
" EasyMock.expect(experimentation.isToggleEnabled(\"OTHER_FLAG\")).andReturn(true).times(5, 6);",
" EasyMock.expect(experimentation.isToggleDisabled(\"OTHER_FLAG\")).andReturn(true).once();",
" EasyMock.expect(experimentation.isToggleDisabled(\"OTHER_FLAG\")).andReturn(true).atLeastOnce();",
" EasyMock.expect(experimentation.isToggleDisabled(\"OTHER_FLAG\")).andReturn(true).anyTimes();",
"// JUnit Assert Test scearios",
" assertFalse(experimentation.isToggleDisabled(\"STALE_FLAG\"));",
" assertTrue(experimentation.isToggleEnabled(\"STALE_FLAG\"));",
" assertFalse(experimentation.isToggleEnabled(\"OTHER_FLAG\"));",
" assertTrue(experimentation.isToggleDisabled(\"OTHER_FLAG\"));",
" assertTrue(foobar());",
" }",
" public boolean foobar() { return true;}",
" public boolean someWrapper(Object obj) { return true;}",
" public OngoingStubbing<Boolean> someWhenWrapper(OngoingStubbing<Boolean> x) { return x;}",
"}")
.addOutputLines(
"XPFlagCleanerSinglePositiveCase.java",
"package com.uber.piranha;",
"import static org.mockito.Mockito.when;",
"import org.mockito.invocation.InvocationOnMock;",
"import org.mockito.stubbing.Answer;",
"import org.mockito.stubbing.OngoingStubbing;",
"import org.easymock.EasyMock;",
"import static org.junit.Assert.assertFalse;",
"import static org.junit.Assert.assertTrue;",
"class MockitoTest {",
" private XPTest experimentation;",
" public void evaluate() {",
"// Mockito Test Scenarios",
" Answer ans = new Answer<Integer>() {\n"
+ " public Integer answer(InvocationOnMock invocation) throws Throwable {\n"
+ " return (Integer) invocation.getArguments()[0];\n"
+ " }};",
"",
"",
"",
"",
"",
" boolean b1 = someWrapper(when(true).thenCallRealMethod());",
" boolean b2 = someWrapper(when(experimentation.isToggleDisabled(\"OTHER_FLAG\")).thenCallRealMethod());",
"",
" someWhenWrapper(when(experimentation.isToggleDisabled(\"OTHER_FLAG\"))).thenCallRealMethod();",
" when(experimentation.isToggleEnabled(\"OTHER_FLAG\")).thenReturn(false);",
" when(experimentation.isToggleEnabled(\"OTHER_FLAG\")).thenThrow(new RuntimeException());",
" when(experimentation.isToggleDisabled(\"OTHER_FLAG\")).thenCallRealMethod();",
" when(experimentation.isToggleEnabled(\"OTHER_FLAG\")).then(ans);",
" when(foobar()).thenReturn(false);",
" when(foobar()).thenAnswer(ans);",
"// Easymock Test scenarios",
"",
"",
"",
"",
"",
"",
" EasyMock.expect(experimentation.isToggleEnabled(\"OTHER_FLAG\")).andReturn(true).anyTimes();",
" EasyMock.expect(foobar()).andReturn(true).times(5);",
" EasyMock.expect(experimentation.isToggleEnabled(\"OTHER_FLAG\")).andReturn(true).times(5, 6);",
" EasyMock.expect(experimentation.isToggleDisabled(\"OTHER_FLAG\")).andReturn(true).once();",
" EasyMock.expect(experimentation.isToggleDisabled(\"OTHER_FLAG\")).andReturn(true).atLeastOnce();",
" EasyMock.expect(experimentation.isToggleDisabled(\"OTHER_FLAG\")).andReturn(true).anyTimes();",
"// JUnit Assert Test scearios",
"",
"",
" assertFalse(experimentation.isToggleEnabled(\"OTHER_FLAG\"));",
" assertTrue(experimentation.isToggleDisabled(\"OTHER_FLAG\"));",
" assertTrue(foobar());",
" }",
" public boolean foobar() { return true;}",
" public boolean someWrapper(Object obj) { return true;}",
" public OngoingStubbing<Boolean> someWhenWrapper(OngoingStubbing<Boolean> x) { return x;}",
"}")
.doTest();
}

@Test
public void testMethodChainTreated() {
String staleFlag = "stale_flag";
Expand Down