Bug 195638 - [compiler][null][refactoring] Wrong error : "Null pointer access: The variable xxx can only be null at this location " with try..catch in loop
Summary: [compiler][null][refactoring] Wrong error : "Null pointer access: The variabl...
Status: VERIFIED DUPLICATE of bug 453483
Alias: None
Product: JDT
Classification: Eclipse Project
Component: Core (show other bugs)
Version: 3.3   Edit
Hardware: PC Windows XP
: P5 normal with 2 votes (vote)
Target Milestone: 4.5 M4   Edit
Assignee: Stephan Herrmann CLA
QA Contact: Ayushman Jain CLA
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2007-07-06 05:49 EDT by Patrick Brühlmann CLA
Modified: 2014-12-09 10:29 EST (History)
11 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Patrick Brühlmann CLA 2007-07-06 05:49:31 EDT
Build ID: I20070625-1500

Steps To Reproduce:
The following "dummy" code sample gives a compilation error :

      String str = null;
      for (int i = 0; i < 2; i++) {
        try {
          str = new String("Test");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        str.charAt(i); // Error : "Null pointer access: The
                       // variable str can only be null at 
                       // this location"
        str = null;
      }


More information:
In my point of view, this compilation error should be replaced with a warning (if so configured) that the variable str COULD be null (in case of catched exception).
Comment 1 Maxime Daniel CLA 2007-07-06 07:46:28 EDT
Reproduced with 3.3.
Comment 2 Maxime Daniel CLA 2007-09-10 03:46:02 EDT
The diagnostic is right, we should have a may be null diagnostic here.
Released NullReferenceTest#746 (inactive) and 747 to illustrate the issue.

If you contrast the original test case with the following, which behaves as expected:
      String str = null;
      for (int i = 0; i < 2; i++) {
        try {
          str = new String("Test");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        str.charAt(i); // Error : "Null pointer access: The
                       // variable str can only be null at 
                       // this location"
        // str = null;
      }
you get the key for the bug. In fact, the checks for str.charAt are deferred (since we are within a loop), and the deferred check mistakenly considers that str can only be null - which is true at the end of the loop, but not when charAt is called.
This is shading light to yet another limit of our current implementation, that is not easy to overcome. The current plans have no allowance for the corresponding investments, hence moving to P5.
Comment 3 Nikolay Metchev CLA 2008-01-25 09:50:00 EST
The following code exhibits the same behavior. Should I file a separate bug report?

import java.sql.Connection;
  void m() throws SQLException
  {
    Connection conn = null;
    try
    {
      conn = createConnection();

      for (; ; )
      {
        throwSomething();
      }
    }
    catch (MyException e)
    {
      conn.rollback(); //The variable can never be null here...
    }
  }

  private void throwSomething() throws MyException
  {
    throw new MyException();
  }

  class MyException extends Exception
  {

  }

  private Connection createConnection()
  {
    return ...;
  }
Comment 4 Maxime Daniel CLA 2008-01-28 02:28:26 EST
Since we do not plan to invest in that area for now, my take would be to keep your additional test case here. If we undertake a major effort in the null diagnostics area and tackle this bug, it will be soon enough to double-check whether your test case is a variation of the same bug or a separate one.
Thanks for sharing the test case anyway.
Comment 5 Michael Schierl CLA 2008-08-21 09:15:59 EDT
Another test case where dummy can never be null:

public class StrangeWarning {
	public static void main(String[] args) {
		String dummy = null;
		for (int i = 0; i < 10; i++) {
			if (i % 2 == 1) {
				dummy = "Foo";
			}
			System.out.println("Hello");
			if (i % 2 == 1) {
				System.out.println(dummy.toLowerCase());
			}
			dummy = null;
		}
	}
}

Workaround to make the warning disappear (if you cannot remove the "dummy = null" code for any reason:

public class StrangeWarningWorkaround {
	public static void main(String[] args) {
		String dummy = null;
		for (int i = 0; i < 10; i++) {
			if (i % 2 == 1) {
				dummy = "Foo";
			}
			System.out.println("Hello");
			if (dummy != null && i % 2 == 1) {
				System.out.println(dummy.toLowerCase());
			}
			dummy = null;
		}
	}
}
Comment 6 Greg Hutchinson CLA 2008-08-22 10:22:04 EDT
I think we have another case where this is not working correctly. In my opinion, this should be changed to "could be null".  Actually, the way this method is written it can never be null...


public class CanOnlyBeNullShouldBeMayBeNull {

	private void method() {
		String tblVarRpl = null;
		while (true) {
			boolean isOpenVariableMortageRateProduct = true;
			boolean tblVarRplAllElementAddedIndicator = false;
			if (isOpenVariableMortageRateProduct) {
				if (tblVarRplAllElementAddedIndicator == false)
					tblVarRpl = "";
				tblVarRpl.substring(1);	//Can only be null???
				return; 
			}
		}
	}
}
Comment 7 Robert Wenner CLA 2009-04-10 12:02:47 EDT
Yet another one:

    public void doStuff() throws Exception {
        Exception e = null;
        try {
            doOtherStuff();
            return;
        }
        catch (Exception oops) {
            oops = e;
        }
        throw e; // wrong warning here
    }
    
    public void doOtherStuff() throws Exception {
        //...
    }
Comment 8 Robert Wenner CLA 2009-04-10 12:04:12 EDT
Actually, with Ganymede, I get the "may be null" warning, but still it's a wrong and misleading warning.
Comment 9 Michael Schierl CLA 2009-04-10 13:58:29 EDT
In #7, the code should be

e = oops;

and not

oops = e;

shouldn't it? And in this case I don't get any warning.
Comment 10 Robert Wenner CLA 2009-04-15 09:45:20 EDT
Sorry, I removed too much from the code. The code below triggers a "Potential null pointer access: The variable e may be null at this location" while that's impossible.

    public void doStuff() throws Exception {
        Exception e = null;
        for (int i = 0; i < 3; i++) {
            try {
                doOtherStuff();
                return;
            }
            catch (Exception oops) {
                e = oops;
            }
        }
        throw e; // wrong warning here
    }

    public void doOtherStuff() throws Exception {
        //...
    }

Robert
Comment 11 Peter Arrenbrecht CLA 2009-05-05 05:26:57 EDT
Another case:

package ch.arrenbrecht.test;

import java.io.IOException;

import junit.framework.TestCase;

public class NullInferenceBug extends TestCase {

	public void testNullRef() throws Exception {
		doNullRef( true );
	}

	public void doNullRef( boolean fail ) throws Exception {
		Object v = null;
		try {
			try {
				v = "Hello";
			}
			finally {
				if (fail)
					throw new IOException();
			}
		}
		catch (IOException e) {
			assertEquals( "Hello", v.toString() );
		}
	}

}
Comment 12 Michael Schierl CLA 2009-07-27 15:08:31 EDT
still present in 3.5 (for some strange bugzilla reason I cannot update the version number above...)
Comment 13 Ayushman Jain CLA 2009-12-11 06:52:25 EST
(In reply to comment #6)
> public class CanOnlyBeNullShouldBeMayBeNull {
> 
>     private void method() {
>         String tblVarRpl = null;
>         while (true) {
>             boolean isOpenVariableMortageRateProduct = true;
>             boolean tblVarRplAllElementAddedIndicator = false;
>             if (isOpenVariableMortageRateProduct) {
>                 if (tblVarRplAllElementAddedIndicator == false)
>                     tblVarRpl = "";
>                 tblVarRpl.substring(1);    //Can only be null???
>                 return; 
>             }
>         }
>     }
> }

In the above case, the return statement is the one causing the problem. If it is removed, we get the correct warning. This is because the way static analysis proceeds is as follows: In a loop, all statements are analysed one by one, and all the null checks are deferred until we've analysed all statements. So here, when we encounter the if else block inside which tblVarRpl is initialised as "", we do mark it as maybe null. Proceeding forward without the return statement would have invoked the deferred checks at the end of loop and correctly reported the maybe null status. Now, here, as soon as we hit the return statement, we mark the flowInfo of the block which contains the return statement as FlowInfo.DEAD_END ie. we assume that if are going ahead of that block in the execution then we wouldn't have entered the block at all. So with the marking of the flowInfo as DEAD_END, all the analysis for that block is rejected, effectively causing the maybe null status of tblVarRpl to be erased. So now when the deffered null check is done, it finds the status to be still NULL, coming from the declaration statement itself.

A possible strategy to counter this would be to invoke the deferred null checks as soon as we hit the return statement. This would correctly report the maybe null status of tblVarRpl. However, it would break the following case : 

public class X {
	               void foo (boolean b) {
			    Object o = null;
			    for (int i = 0; i < 25; i++) {
			      if (b) {
			        if (o == null) {   //should report always null
			          o = new Object();
			        }
			        return;
			      }
			    }
			  }
			}

This is because here after the if(o==null)... else... block the status of 'o' becomes surely not null and now if we invoke the deferred check at return statement itself, it'll act on the flowInfo of the if(b).. block  - which has 'o' set to non null. This gives a never null warning for 'o' in if(o == null). However, the ideal behavior should be to give an always null warning on 'o' in if(o == null).

So the correct strategy would be to NOT defer the null check when we have a return statement in the block , and to defer it in case we don't. However, there's no way to predict that a return statement will be found ahead, and hence no easy to way fix this predicament.
Comment 14 Shaun Spiller CLA 2010-12-09 10:44:41 EST
Here is a very simple demonstration of the bug with no try/catch:

	private void test() {
		boolean x = true;
		Object o = null;
		
		for (;;) {
			if (x) o = new Object();
			
			o.toString(); // warning here
			
			o = null;
		}
	}

If boolean x is made final or the if condition is otherwise made into a hard constant like 1 == 1, the problem disappears. If the o = null line at the end of the loop or the looping itself is removed, that also makes the problem disappear.
Comment 15 Ayushman Jain CLA 2010-12-09 14:32:41 EST
(In reply to comment #14)
> [..]

Thanks Shaun. This looks like a different case to me. Can you please file another bug report for this? Thanks!
Comment 16 Shaun Spiller CLA 2010-12-09 18:20:29 EST
(In reply to comment #15)
> Thanks Shaun. This looks like a different case to me. Can you please file
> another bug report for this? Thanks!

Someone can if they want, but it looks like exactly the same bug to me.
Comment 17 Ayushman Jain CLA 2010-12-10 00:12:40 EST
(In reply to comment #16)
> (In reply to comment #15)
> > Thanks Shaun. This looks like a different case to me. Can you please file
> > another bug report for this? Thanks!
> 
> Someone can if they want, but it looks like exactly the same bug to me.

The presence / absence of try catch block does make a difference in the static analysis, that why i suggested its better to investigate this issue separately. Anyway, i'll look into it and open a fresh bug if this indeed turns out to be different. Thanks.
Comment 18 Stephan Herrmann CLA 2011-02-25 08:31:57 EST
(In reply to comment #14)
> Here is a very simple demonstration of the bug with no try/catch:
> 
>     private void test() {
>         boolean x = true;
>         Object o = null;
> 
>         for (;;) {
>             if (x) o = new Object();
> 
>             o.toString(); // warning here
> 
>             o = null;
>         }
>     }

This might be related to my recent findings in bug 336428 comment 6:
When analyzing the loop body, we don't have the information from
x=true at hand (because we don't yet know whether this will also hold
on subsequent iterations.

In bug 336428 I proposed to investigate this post 3.7, because in the
current design not much can be done here.
Comment 19 Dwight Harm CLA 2014-03-25 15:13:19 EDT
This may be the same bug, but doesn't require try/catch and doesn't require "x=null" at bottom of loop. In any case, maybe an additional test case is worthwhile:

	public void testIt() {
	  Object aRole = null;
	  for (;;) {
	    aRole = new Object();
	    if (aRole.toString() == null) {
	      aRole = getObject(); // changing to "new Object()" makes warning disappear.
	    }
	    aRole.toString();
	    // above line gets: "Null pointer access: The variable aRole can only be null at this location"
	    break;
	  }
	}
	private Object getObject() {
	  return new Object();
	}

This is failing in 3.9.1.
Comment 20 Stephan Herrmann CLA 2014-11-29 12:36:07 EST
Bug 453483 brings new hope for this old bug, too. At least the initial test NRT._test0746_for_try_catch passes with my pending changes. Other examples will be checked as we go.
Comment 21 Stephan Herrmann CLA 2014-11-30 12:57:58 EST
Changing dependency to 'duplicate'. Additional examples still to be codified as tests.

*** This bug has been marked as a duplicate of bug 453483 ***
Comment 22 Stephan Herrmann CLA 2014-11-30 17:05:12 EST
(In reply to Nikolay Metchev from comment #3)
> The following code exhibits the same behavior. Should I file a separate bug
> report?
> 
> import java.sql.Connection;
>   void m() throws SQLException
>   {
>     Connection conn = null;
>     try
>     {
>       conn = createConnection();
> 
>       for (; ; )
>       {
>         throwSomething();
>       }
>     }
>     catch (MyException e)
>     {
>       conn.rollback(); //The variable can never be null here...
>     }
>   }
> 
>   private void throwSomething() throws MyException
>   {
>     throw new MyException();
>   }
> 
>   class MyException extends Exception
>   {
> 
>   }
> 
>   private Connection createConnection()
>   {
>     return ...;
>   }

This problem was already fixed in 3.6.2.

It was likely resolved by the fix for bug 332637.
Comment 23 Stephan Herrmann CLA 2014-11-30 17:08:36 EST
(In reply to Michael Schierl from comment #5)
> Another test case where dummy can never be null:
> 
> public class StrangeWarning {
> 	public static void main(String[] args) {
> 		String dummy = null;
> 		for (int i = 0; i < 10; i++) {
> 			if (i % 2 == 1) {
> 				dummy = "Foo";
> 			}
> 			System.out.println("Hello");
> 			if (i % 2 == 1) {
> 				System.out.println(dummy.toLowerCase());
> 			}
> 			dummy = null;
> 		}
> 	}
> }

This would require for the compiler to understand the correlation between the two if statements. This is beyond the scope of our analysis.
Comment 24 Stephan Herrmann CLA 2014-11-30 17:18:10 EST
(In reply to Robert Wenner from comment #10)
> Sorry, I removed too much from the code. The code below triggers a
> "Potential null pointer access: The variable e may be null at this location"
> while that's impossible.
> 
>     public void doStuff() throws Exception {
>         Exception e = null;
>         for (int i = 0; i < 3; i++) {
>             try {
>                 doOtherStuff();
>                 return;
>             }
>             catch (Exception oops) {
>                 e = oops;
>             }
>         }
>         throw e; // wrong warning here
>     }
> 
>     public void doOtherStuff() throws Exception {
>         //...
>     }
> 
> Robert

Needs investigation. I put a note into bug 370424.
Comment 25 Stephan Herrmann CLA 2014-11-30 17:21:57 EST
(In reply to Peter Arrenbrecht from comment #11)
> Another case:
> 
> package ch.arrenbrecht.test;
> 
> import java.io.IOException;
> 
> import junit.framework.TestCase;
> 
> public class NullInferenceBug extends TestCase {
> 
> 	public void testNullRef() throws Exception {
> 		doNullRef( true );
> 	}
> 
> 	public void doNullRef( boolean fail ) throws Exception {
> 		Object v = null;
> 		try {
> 			try {
> 				v = "Hello";
> 			}
> 			finally {
> 				if (fail)
> 					throw new IOException();
> 			}
> 		}
> 		catch (IOException e) {
> 			assertEquals( "Hello", v.toString() );
> 		}
> 	}
> 
> }

Likewise: This problem was already fixed in 3.6.2.

It was likely resolved by the fix for bug 332637.
Comment 26 Michael Schierl CLA 2014-11-30 17:47:36 EST
(In reply to Stephan Herrmann from comment #23)
> (In reply to Michael Schierl from comment #5)
> > Another test case where dummy can never be null:
> > 
> > public class StrangeWarning {
> > 	public static void main(String[] args) {
> > 		String dummy = null;
> > 		for (int i = 0; i < 10; i++) {
> > 			if (i % 2 == 1) {
> > 				dummy = "Foo";
> > 			}
> > 			System.out.println("Hello");
> > 			if (i % 2 == 1) {
> > 				System.out.println(dummy.toLowerCase());
> > 			}
> > 			dummy = null;
> > 		}
> > 	}
> > }
> 
> This would require for the compiler to understand the correlation between
> the two if statements. This is beyond the scope of our analysis.

I am fine with a message that warns about `dummy` potentially being null. As of Luna (4.4.0) this complains that `dummy` can *only* be null at that location, which is definitely wrong and the opposite of what happens in reality.
Comment 27 Stephan Herrmann CLA 2014-11-30 17:53:18 EST
With bug 453483 fixed I've released various examples from this bug as regression tests:

http://git.eclipse.org/c/jdt/eclipse.jdt.core.git/commit/?id=2ce0d6de6bed41247718345892752d1c05e3cb7e

Includes
 - comment 3  - was already fixed in 3.6.2
 - comment 6  - fixed via bug 453483
 - comment 14 - fixed via bug 453483
 - comment 19 - fixed via bug 453483

(Comment 0 already existed as NRT.test0746_for_try_catch).
Comment 28 Stephan Herrmann CLA 2014-11-30 17:55:48 EST
(In reply to Michael Schierl from comment #26)
> (In reply to Stephan Herrmann from comment #23)
> > (In reply to Michael Schierl from comment #5)
> > > Another test case where dummy can never be null:
> > > 
> > > public class StrangeWarning {
> > > 	public static void main(String[] args) {
> > > 		String dummy = null;
> > > 		for (int i = 0; i < 10; i++) {
> > > 			if (i % 2 == 1) {
> > > 				dummy = "Foo";
> > > 			}
> > > 			System.out.println("Hello");
> > > 			if (i % 2 == 1) {
> > > 				System.out.println(dummy.toLowerCase());
> > > 			}
> > > 			dummy = null;
> > > 		}
> > > 	}
> > > }
> > 
> > This would require for the compiler to understand the correlation between
> > the two if statements. This is beyond the scope of our analysis.
> 
> I am fine with a message that warns about `dummy` potentially being null. As
> of Luna (4.4.0) this complains that `dummy` can *only* be null at that
> location, which is definitely wrong and the opposite of what happens in
> reality.

I just checked: the fix indeed changes the warning from "can only" to "may be". So all looks well :)
Comment 29 shankha banerjee CLA 2014-12-09 10:29:58 EST
Verified for 4.5M4 using I20141208-2000 build.