Skip to content

chore: add checkerframework for nullness verification#1814

Merged
vlsi merged 3 commits intopgjdbc:masterfrom
vlsi:checkerframework
Jul 18, 2020
Merged

chore: add checkerframework for nullness verification#1814
vlsi merged 3 commits intopgjdbc:masterfrom
vlsi:checkerframework

Conversation

@vlsi
Copy link
Copy Markdown
Member

@vlsi vlsi commented Jul 3, 2020

See https://checkerframework.org/

Let's see how it works. The intention is to have non-nullable types by default, and add @Nullable when the returned type is nullable.

String nonNullable;
@Nullable String nullable;

String[] // array and elements: non-nullable
String @Nullable [] // array: nullable, elements: non-nullable
@Nullable String[] // array: non-nullable, elements: nullable
@Nullable String @Nullable [] // array: nullable, elements: nullable

So far I'm not sure if we can use our own nullability annotations.

Dependencies

The added dependency checkerframework:checker-qual:3.5.0. It is used for annotations only, so the dependency can be excluded.
Note: jre6, jre7, and source release are produced with all the annotations commented out.

Issues

Checker Framework relies on the annotated JDK for the verifications, and there might be issues there.
For instance, BatchUpdateException is not annotated with @Nullable

typetools/jdk#62
typetools/jdk#63
typetools/jdk#64

In practice, we can overrule that by placing stub files to config/checkerframework/... folder, so we do not depend on CF releases.

https://youtrack.jetbrains.com/issue/IDEA-245323 -- IDEA produces false positives for nested arrays.

Findings

CheckerFramework works on a method by method basis, so it seems to produce lots of errors when the precondition is verified in another method.

For instance:

class PgResultSet {
  @Nullable Map<String, Object> updateValues = ...;

  @EnsuresNonNull("updateValues")
  private void checkUpdateable() throws SQLException {
    ...
    if (updateValues == null) {
      // allow every column to be updated without a rehash.
      updateValues = new HashMap<String, Object>((int) (fields.length / 0.75), 0.75f);
    }
  }

  protected void updateValue(@Positive int columnIndex, @Nullable Object value) throws SQLException {
    checkUpdateable();
    ...
    updateValues.put("column_name", value); // <-- checkerframework warns that updateValues can be null
  }

As far as I understand, EnsuresNonNull should tell CF that updateValues is non-null if checkUpdateable() completes successfully. However, it does not seem to work, and, on top of that, IDEA does not seem to recognize @EnsuresNonNull either.

The same goes for if (wasNull()) { return null; } ... getString(...) which often appears in getters.
We know getString can't return null, however, CF produces warnings.

Edge cases

public ByteBuffer read() throws SQLException {
checkClose();
ByteBuffer payload = null;
while (payload == null && copyDual.isActive()) {
payload = readInternal(true);
}
return payload;

read() Javadoc says the method never returns null, yet it would return null in case copyDual.isActive() becomes false.

Test code

It looks like verifying test code takes too much time, and the gain does not seem to be significant enough, so the test code is not verified.

@vlsi vlsi force-pushed the checkerframework branch from 9e92d03 to 6b59cb4 Compare July 3, 2020 19:27
@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 3, 2020

Just in case, the very first error is a true bug:

src/main/java/org/postgresql/ssl/SingleCertValidatingFactory.java:101: error: [dereference.of.nullable] dereference of possibly-null reference Thread.currentThread().getContextClassLoader()
            Thread.currentThread().getContextClassLoader().getResourceAsStream(path));
                                                        ^

getContextClassLoader indeed might return null, so the code might fail with NPE.

@davecramer
Copy link
Copy Markdown
Member

cool!

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jul 3, 2020

Codecov Report

Merging #1814 into master will decrease coverage by 0.14%.
The diff coverage is 63.26%.

@@             Coverage Diff              @@
##             master    #1814      +/-   ##
============================================
- Coverage     68.73%   68.59%   -0.15%     
- Complexity     4223     4231       +8     
============================================
  Files           195      196       +1     
  Lines         17578    17740     +162     
  Branches       2892     2939      +47     
============================================
+ Hits          12083    12168      +85     
- Misses         4192     4219      +27     
- Partials       1303     1353      +50     

@sehrope
Copy link
Copy Markdown
Member

sehrope commented Jul 4, 2020

Very cool. Does this stick around as a runtime dependency or is it only test / build time?

@sehrope
Copy link
Copy Markdown
Member

sehrope commented Jul 4, 2020

Just in case, the very first error is a true bug:

src/main/java/org/postgresql/ssl/SingleCertValidatingFactory.java:101: error: [dereference.of.nullable] dereference of possibly-null reference Thread.currentThread().getContextClassLoader()
            Thread.currentThread().getContextClassLoader().getResourceAsStream(path));
                                                        ^

getContextClassLoader indeed might return null, so the code might fail with NPE.

Nice catch. I doubt that'd happen in practice as you'd have to go out of your way to end up with a null value.

Still nice to fix it, what should the fallback be? Classloader of the class itself (this.getClass().getClassLoader())?

@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 4, 2020

There are reasons to keep the annotations in the published jars: other languages and tools would see the annotations and infer nullability of the parameters and the returned values. I know Kotlin rejects the code if you pass null to a Java parameter that is annotated as not null.

I am not sure yet which annotation library to choose (there are multiple), however, the switch can probably be made by a global search and replace.

There's a backfire from Java 1.7 though: it does not support type annotations, so we can't have Map<@Nullable String, String> there. I guess either we drop 1.7 support or we add "replace @Nullable with /*@Nullable*/" step there.

@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 4, 2020

@sehrope , I've improved classloader handling in 5937065#diff-b5476fa4ea51ca4548c14b11c8ec4179R100

@sehrope
Copy link
Copy Markdown
Member

sehrope commented Jul 4, 2020

Good point on keeping them in. I took a peek at the license for this one, it's GPL with classpath exception so should be good right?

+1 for anything that gives us more reasons to drop 1.7 support. We have to drop it at some point, let's pick something and move on. It's been 5+ years that's it's EOL.

@sehrope
Copy link
Copy Markdown
Member

sehrope commented Jul 4, 2020

@sehrope , I've improved classloader handling in 5937065#diff-b5476fa4ea51ca4548c14b11c8ec4179R100

LGTM. That looks like it could go in on it's own as it's own PR without the rest of the new annotations. I was going to suggest moving that logic into it's own method but the only other code reading a resource stream is in a test so not worth it.

@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 6, 2020

@sehrope , @bokken , @davecramer , do you have something to say regarding the changes like a688233 and d435f3b ?

The change is like

  public void flush() throws IOException {
-   checkClosed();
+   LargeObject lo = checkClosed();
    try {
      if (bpos > 0) {
        lo.write(buf, 0, bpos);

It looks like LargeObject lo = checkClosed() can provide IDE and checker the information that lo can't be null in this context which sounds nice.

On the other hand, then lo = checkClosed() makes it a little bit harder to "find lo field usages".

Frankly speaking, I'm inclined to LargeObject lo = checkClosed() style as it makes it easier to see if lo is usable at all.

@davecramer
Copy link
Copy Markdown
Member

@sehrope , @bokken , @davecramer , do you have something to say regarding the changes like a688233 and d435f3b ?

The change is like

  public void flush() throws IOException {
-   checkClosed();
+   LargeObject lo = checkClosed();
    try {
      if (bpos > 0) {
        lo.write(buf, 0, bpos);

It looks like LargeObject lo = checkClosed() can provide IDE and checker the information that lo can't be null in this context which sounds nice.

On the other hand, then lo = checkClosed() makes it a little bit harder to "find lo field usages".

Frankly speaking, I'm inclined to LargeObject lo = checkClosed() style as it makes it easier to see if lo is usable at all.

Looking at this I see no reason not to do it. Did you have specific concerns ?

@sehrope
Copy link
Copy Markdown
Member

sehrope commented Jul 8, 2020

... do you have something to say regarding the changes like a688233

+1. Much cleaner.

... and d435f3b ?

I don't like this one as much, in particular the shading of the member variables.

Can instead make the lo member variable not-null (assigned only at constructor) and change checkClosed() to check the lo's own closed property? I don't think it's accessible but that's an easy fix to expose it. They're in the same package too so no need to expose it further than package level.

@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 8, 2020

Can instead make the lo member variable not-null

I thought behind the same lines as well, however, there are cases when nullifying might help to release resources.

@bokken
Copy link
Copy Markdown
Member

bokken commented Jul 8, 2020

It is definitely more of a style thing, but I kind of like the changes in d435f3b
I might suggest making the local LargeObject lo as final.

@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 9, 2020

Ok, less than 100 errors left :)

96 errors
2 warnings

@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 10, 2020

Open question: is it legal to return null from org.postgresql.jdbc.PgArray#getArray() (it implements java.sql.Array#getArray() ) ?

Javadoc specifies:

   * @return an array in the Java programming language that contains
   * the ordered elements of the SQL <code>ARRAY</code> value
   * designated by this <code>Array</code> object

The current code has return null branch:

if (fieldString == null) {
return null;
}


When receiving the array from DB, I expect that ResultSet.getArray() should return nullable Array rather than Array with a nullable contents.
So when receiving data from the DB it would be better to have non-nullable contents.

When sending data to the DB, the current API allows to pass null to Connection#createArrayOf(String typeName, Object[] elements) elements :-(
The current implementation treats conn.createArrayOf("float8", null) as if it were cast(null as float8[]).

Unfortunately, we have a single class PgArray only for doing both reads and writes, so we can't have nullable getArray and non-nullable getArray at the same time :(

Any thoughts?

@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 10, 2020

One more item: CopyManage#copyIn(String sql), CopyManage#copyOut(String sql), CopyManage#copyDual(String sql)

public CopyIn copyIn(String sql) throws SQLException {
CopyOperation op = queryExecutor.startCopy(sql, connection.getAutoCommit());
if (op == null || op instanceof CopyIn) {
return (CopyIn) op;

I wonder if the following should be converted to an error rather than a silent log:

if ( processingCopyResults.compareAndSet(false,true) == false ) {
LOGGER.log(Level.INFO, "Ignoring request to process copy results, already processing");
return null;
}

@vlsi vlsi force-pushed the checkerframework branch 3 times, most recently from f9abdd7 to 7be56cf Compare July 11, 2020 12:10
@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 11, 2020

AFAIK the PR is more-or-less ready. It builds (including jre6, jre7, and dependency-reduced-pom).

@vlsi vlsi force-pushed the checkerframework branch 8 times, most recently from 28cf119 to 1231a79 Compare July 11, 2020 19:39
@vlsi
Copy link
Copy Markdown
Member Author

vlsi commented Jul 11, 2020

I think the PR is ready for merge, and I'm inclined to merge it in 2-3 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants