-
Notifications
You must be signed in to change notification settings - Fork 92
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
Fixing bug report #39 [extract local] Extract to local variable may result in NullPointerException. #66
Fixing bug report #39 [extract local] Extract to local variable may result in NullPointerException. #66
Conversation
@jjohnstn Would it be convenient for you to review this PR? Thanks a lot! |
Looking at it now. I added a comment in the initial issue about what I think the proper fix should be. |
Based on the suggestions in the issue, We optimized the process of the refactoring when we perform NullPointerException checking. Besides, three test cases have been appended to verify the changes, which are available in |
/rebase |
cd12c64
to
cb8359c
Compare
Thank you very much. If you have any requirements, we are willing to help. |
/rebase |
3147b0d
to
8fca7bc
Compare
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.
Hi, I have a chance to look at the code in more detail now that it is building properly. There are logic flaws in using the toString() method for an ASTNode (see comments in NullChecker.java) and I think the code should avoid adding the local var in the case where there is only one use and adding the variable will precede the null check. Never adding the invalid statement would negate the need for a warning message. Most users will not preview and will simply hit Ok so would never get to see the warning. I don't think it is an issue to omit as most users would never select a single use method call to be extracted to a local variable and your code already does the right thing when there are multiple uses.
If you need help in making the logic correct in NullChecker, let me know. As mentioned, the right way to verify a match is to find a Name and then resolve it's binding at which point you can compare the bindings. Note that resolving a binding may end up returning null in which case, you should probably bail (this can happen if the code has errors). You can make it more robust by using the Name.getFullyQualifiedName() method and don't bother asking for bindings until you get a potential match.
} | ||
@Override | ||
public void preVisit(ASTNode node) { | ||
if(node instanceof MethodInvocation) { |
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 logic will not work reliably. The toString() method is not a valid way to determine equality. For example, you could have casting or parenthesized expressions. In addition, variables/fields can have the same names (e.g. a local variable in a block overriding another local variable outside the block). You need to find a Name and then resolve its binding. Then you can verify that the binding of A.isEqualTo(B).
} | ||
|
||
@Override | ||
public boolean preVisit2(ASTNode node) { |
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.
You also need to check for instanceof expressions as these will weed out nulls as well. e.g. if (!(a instanceof String)) { return null; } else {((String)a).length();}
this.nullFlag=false; | ||
} | ||
|
||
public boolean isNull() { |
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.
I think a better name could be helpful. For example, hasNullCheck()
this.invocationSet=iv.invocationSet; | ||
} | ||
|
||
public boolean isExistNull( ) { |
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.
I think a better name for this could be helpful. hasNullCheck() for example.
Expression leftExpression = infixExpression.getLeftOperand(); | ||
Expression rightExpression = infixExpression.getRightOperand(); | ||
Expression target=null; | ||
if( rightExpression.getNodeType()==ASTNode.NULL_LITERAL) { |
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.
If you do a CTRL+Shift+F you can eliminate some formatting issues (like spaces between if and open brackets, etc..
|
||
class A { | ||
public int m(int[] arr) { | ||
int length= arr.length; |
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.
I think the logic should avoid doing anything for this particular test. This makes it consistent with the case where there are two references and it moves it beyond the if statement. Most people will not do a preview and won't see the warning.
I also ran into another scenario:
public void foo(String s) {
if (s == null || s.length() == 0) {
System.out.println("here");
} else {
int k = s.length();
System.out.println(k);
}
int z = s.length();
System.out.println(z);
}
If I select the first s.length(), the scope of that should be enough to add a local declaration outside the if statement and then set z = length; Instead, it only finds the s.length() calls in the else statement and adds a local variable there, but not for the int z case.
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.
Suppose we add a local declaration 'length=s.length()' before the if statement, it will result in nullpointer exception. However, if we add the declaration after the else statement (just before the declaration of z, we cannot replace the occurence of 's.length()' within the else statement with the new variable.
Although it is possible to declare the variable (but without initialization) before the if statement, it could be clumsy. There is no guarantee whether the variable has been initialized when it is assessed: Initializing it on the else branch only may not guarantee that it has been initialized after the whole if statement. To this end, we should initialized the same variable multiple times (first on the else branch, and then after the whole if statement). I don't think it is better than employing different variables to represent the occurrences within else statements and those outside the statement, respectively.
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.
A possible solution here is: replace the expressions within the else statement only (as what the commit does).
A better solution: Don't do anything here. It is odd to delcare (and initialize) a variable and the only use of the variable is to intialize another variable in the form of VariableA=VarialbeB.
`
int length=s.length();
int k = length;
System.out.println(k);`
Thanks for your patient reply. I will read the details and fix the flaws as soon as possible. |
Hi, we have carefully read your comments and made the following revisions to the code. 1. Remove Warning MessageWe remove the warning message when only one expression is extracted and such extracting would result in NullPointerExecption. Correspondingly, we modified test-119 so that the refactored code is the same as the source code. Notably, since 2. Replacing String Matching with Combination of Name Binding and AST MatchingAs you suggested, we match different expressions based on name bindings. Notably, for MethodInvocation node, we can only access its method declaration binding. So we reused API 3. Handling of CastExpression and ParenthesizedExpressionTo deal with CastExpression and ParenthesizedExpression, before we compare each node, we remove the brackets outside the variables/expressions, which is implemented in the method 4. Roughly Ignore InstanceofExpressionAs for InstanceofExpression, you provided a case as follows: '''
if (!(a instanceof String)){
return null;
}
else {
((String)a).length();
}
((String)a).length();
''' I think if expressions |
Thanks for the changes. One test issue I ran into:
When the test above goes to replace s.length() from the if statement and I have chosen to replace all references, it adds an int length= s.length() statement in the else statement which is fine, but it does not add anything to replace the int z = s.length() statement. It will have to add a 2nd statement (e.g. int length2 = s.length()) or abandon changes in the else statement and move the int length = s.length() statement just before the int z statement. |
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.
See my comment about test case.
Hi, the latest changes have been submitted. According to your suggestion, we try to extract as many as possible expressions if public void foo(String s) {
if (s == null || s.length() == 0) {
return;
} else {
int length= s.length();
System.out.println(length);
}
int length_1= s.length();
int z = length_1;
System.out.println(z);
} Assuming that the user selected the second Another thing is that we improve the code, which handles the situation that no expression can be extracted safely. We use TextEditVisitor |
@jjohnstn Could you please review the latest code? Thank you so much! |
072d24f
to
dbe0bd6
Compare
@chixiaye Just reviewing now. The patch needed a refresh to sync with master and I had to force push a rebase. |
- NullChecker should always check a resolveBinding() for null before using - fix ExtractTempRefactoring logic for choosing names to simply append count after variable name to match other cleanups/fixes - add logic to fetch the current list of locals so that we can ensure new name is unique and not already used - add new test to show local name logic will skip over used names
Thanks for your cooperation to fulfill this pr. I look forward to continuing to contribute to Eclipse in the future. |
It's already in "JDT UI Examples" workingset (see line 558) Co-authored-by: Jörg Kubitz <jkubitz-eclipse@gmx.de>
This is a patch based on the solution explained in the bug report.
OverView
In total, this patch changes four files: Three are modified and a new file is added.
We list all involved files and explain the changes made on them:
1. Changes on
RefactoringCoreMessages.java
andrefactoring.properties
Add the warning message about NullPointerException.
2.
ExtractTempRefactoring.java
First, we add three members in the class
ExtractTempRefactoring
and initialize accordingly in the constructors.startPoint
andendPoint
are used to indicate the position of the extracted sequence selected from a to-be-extracted sequence. AndnullWarning
is the NullPointerException flag. Correspondingly, we decide whether to prompt according to the value of the member incheckFinalConditions
method. It's worth noting thatnullWarning
is true if extracting the selected expression must result in an exception.Second, since we will select subsequences with extracted sequences, We modify the parameter list of method
findDeepestCommonSuperNodePathForReplacedNodes
to match.Third, because we're not necessarily extracting the first potential location, we modify the
getFirstReplacedExpression()
function's name and parameter list.Next, the function
reSortRetainOnlyReplacableMatches()
is added to make sure, the replaceable expressions are sorted in ascending order by the offset value.Last, function
addReplaceExpressionWithTemp()
is the main part of our modification. As with our proposed solution in the issue, we use a greedy strategy to obtain replaceable sequences.3.
NullChecker.java
The Class
NullChecker
is added to detect potential NullPointerException between the two given offsets.In the constructor, we initialize the required members and calculate the invocationSet. For example, if expression is
a.b().c
, the invocationSet will containa
,a.b()
. We created the classInvocationVisitor
to finish the task.As for method
isExistNull
, it is used to determine whether there is a potential NullPointerException. It uses classNullMiddleCodeVisitor
to findInfixExpression
such asa==null
. ThenullFlag
member indicates if a potential NullPointerException was detected.