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 support for nullable type upper bounds considering Library mod… #893

Closed

Conversation

akulk022
Copy link
Collaborator

The below line previously reported an error considering the upperbound of the generic type parameter couldn't be Nullable, adding it into library models to allow it to be Nullable passes the test case.

Function<String,@Nullable String> removeA = a -> a.replace("a","");

Copy link

codecov bot commented Jan 10, 2024

Codecov Report

Attention: 7 lines in your changes are missing coverage. Please review.

Comparison is base (c1eb8ca) 86.95% compared to head (f6996d1) 87.07%.
Report is 5 commits behind head on master.

Files Patch % Lines
...away/src/main/java/com/uber/nullaway/NullAway.java 90.00% 0 Missing and 3 partials ⚠️
...ava/com/uber/nullaway/generics/GenericsChecks.java 85.71% 0 Missing and 3 partials ⚠️
...src/main/java/com/uber/nullaway/LibraryModels.java 66.66% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #893      +/-   ##
============================================
+ Coverage     86.95%   87.07%   +0.12%     
- Complexity     1953     1981      +28     
============================================
  Files            77       77              
  Lines          6315     6385      +70     
  Branches       1222     1239      +17     
============================================
+ Hits           5491     5560      +69     
+ Misses          420      419       -1     
- Partials        404      406       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

The approach mostly looks good! I just have one comment on the API structure.

Comment on lines 101 to 103
LibraryModelsHandler libHandler = new LibraryModelsHandler(config);
boolean hasNullableUpperBoundLibModel =
libHandler.nullableUpperBoundExistsInLibModel(baseType.tsym.toString(), i);
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't want to create a new LibraryModelsHandler object here. To be consistent with the rest of the code, I think what we want here is to add a new method to the Handler interface like onOverrideTypeParameterUpperBound. We can implement that method in LibraryModelsHandler. Then we could pass in the handler object from the NullAway class and then call handler.onOverrideTypeParameterUpperBound here.

Copy link
Collaborator Author

@akulk022 akulk022 Jan 11, 2024

Choose a reason for hiding this comment

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

Thanks for the comment. I agree, I didn't want to add the method to the interface till I was sure this is the right approach. I'll add it now.

@akulk022 akulk022 marked this pull request as ready for review January 12, 2024 01:12
Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Couple of quick comments, will look more closely later

* @return map from the className to the position of the generic type argument that has a Nullable
* upper bound.
*/
ImmutableSetMultimap<String, Integer> nullableVariableTypeUpperBounds();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
ImmutableSetMultimap<String, Integer> nullableVariableTypeUpperBounds();
ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBounds();

@@ -97,9 +103,11 @@ public static void checkInstantiationForParameterizedTypedTree(
upperBound.getAnnotationMirrors();
boolean hasNullableAnnotation =
Nullness.hasNullableAnnotation(annotationMirrors.stream(), config);
boolean hasNullableUpperBoundLibModel =
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need a new variable here. We could just do hasNullableAnnotation = Nullness.hasNullableAnnotation(annotationMirrors.stream(), config) || handler.onOverrideTypeParameterUpperBound(baseType.tsym.toString(), i)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

a few more comments

* @return map from the className to the position of the generic type argument that has a Nullable
* upper bound.
*/
ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBounds();
Copy link
Collaborator

Choose a reason for hiding this comment

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

So, thinking more about this change, one thing is that thus far, JSpecify mode changes have all been hidden under a flag and current NullAway users have not been impacted at all. This change would be different, since anyone who built their own LibraryModels implementation will need to implement this method. But, the LibraryModels interface is where this method belongs. I am leaning towards allowing this method here, but maybe we should also provide an abstract class like EmptyLibraryModels that users can subclass, where it would just return empty for everything. What do you think?

Also, maybe we should be more careful about version numbering, and make the next release 0.11.0 to indicate there is a "breaking" change for users who implemented the LibraryModels interface before. @yuxincs do you agree?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So should we also extend the new Empty library models abstract class for the classes currently implementing LibraryModels interface. for e.g the Example Library Models? So that the default implementations are kept in the EmptyLibraryModels.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, that's what I'm thinking. Our current implementations should extend the new empty library models class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks. Makes sense, I'll do that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, really sorry here, but I realized we are going about this the wrong way, and we really should be using the language feature introduced for just this case, default methods in interfaces. There is no need for introducing the EmptyLibraryModels class; we should just make typeVariablesWithNullableUpperBounds() a default method that returns the empty set and then all existing implementations will just work without any changes.

@akulk022 could you do the following:

  1. Make typeVariablesWithNullableUpperBounds() a default method.
  2. Also make LibraryModels.nullableFields() a default method that returns an empty set. We should have done this when introducing this method before but I forgot.
  3. Get rid of EmptyLibraryModels, and undo the changes to ExampleLibraryModels and TestLibraryModels.

Sorry again for not remembering about default methods but they are definitely the best way to go here.

Copy link
Collaborator Author

@akulk022 akulk022 Jan 16, 2024

Choose a reason for hiding this comment

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

No problem, It's a nice language feature to deal with such a problem and I should have known it exists, I will certainly remember it now. I'll do the needful right away.

@@ -305,4 +305,16 @@ public Integer castToNonNullArgumentPositionsForMethod(
}
return previousArgumentPosition;
}

@Override
public boolean onOverrideTypeParameterUpperBound(String className, int arg) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Document in Javadoc that this returns true if any handler returns true

* @return boolean value of regardless of whether the upper bound for that argument has a Nullable
* upper bound.
*/
boolean onOverrideTypeParameterUpperBound(String className, int arg);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
boolean onOverrideTypeParameterUpperBound(String className, int arg);
boolean onOverrideTypeParameterUpperBound(String className, int index);

Comment on lines 1010 to 1013
for (Map.Entry<String, Integer> entry :
libraryModels.typeVariablesWithNullableUpperBounds().entries()) {
nullableVariableTypeUpperBoundsBuilder.put(entry);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use putAll instead of writing a loop

private static class OptimizedLibraryModels {

/**
* Mapping from {@link MethodRef} to some state, where lookups first check for a matching method
* name as an optimization. The {@link Name} data structure is used to avoid unnecessary String
* conversions when looking up {@link com.sun.tools.javac.code.Symbol.MethodSymbol}s.
* conversions when looking up {@link Symbol.MethodSymbol}s.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am not sure why this went in there, It was probably the IDE that did something, I'll revert it.

"import java.util.function.Function;",
"class Test {",
" static void testNegative() {",
" Function<String,@Nullable String> removeA = a -> a.replace(\"a\",\"\");",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's write more tests to show the type argument is being handled correctly. E.g., check that passing in @Nullable when the first type argument is not @Nullable yields an error, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment is still not addressed

Comment on lines 70 to 74
@Override
public ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBounds() {
return ImmutableSetMultimap.of();
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Code like in this class could be the EmptyLibraryModels implementation maybe

Copy link
Collaborator

Choose a reason for hiding this comment

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

@akulk022 we can now delete this method right?

Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

A few more comments

Comment on lines 70 to 74
@Override
public ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBounds() {
return ImmutableSetMultimap.of();
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

@akulk022 we can now delete this method right?

Comment on lines 83 to 87
@Override
public ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBounds() {
return ImmutableSetMultimap.of();
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

With the default method I think we can delete this

Comment on lines 405 to 417
/**
* Called to get the library models which have Nullable upper bound for their Generic Type
* Arguments.
*
* @param className A String containing the name of the class for which we want to check Nullable
* upper bound.
* @param index Indicates the generic type argument for which we want to check Nullable upper
* bound.
* @return boolean value of regardless of whether the upper bound for that argument has a Nullable
* upper bound.
*/
boolean onOverrideTypeParameterUpperBound(String className, int index);

Copy link
Collaborator

Choose a reason for hiding this comment

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

Didn't I suggest some changes in these docs before? Did those changes get lost?

Copy link
Collaborator Author

@akulk022 akulk022 Jan 16, 2024

Choose a reason for hiding this comment

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

I added that in a javadoc for the implementation of the method inside CompositeHandler since it seemed specific to the implementation inside CompositeHandler, would it be more appropriate to mention it here instead?

new ImmutableSetMultimap.Builder<String, Integer>()
.put("java.util.function.Function", 0)
.put("java.util.function.Function", 1)
.put("java.util.Map", 1)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't recall wanting to model java.util.Map in this PR. Why did we add this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, I had added it for a positive test case where only one of the indices has the upper bound and we still fail for the other. But in hindsight it is not the correct way to add a model for it. I'll remove it.

Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Mostly looks good, just one comment remaining to be addressed

"import java.util.function.Function;",
"class Test {",
" static void testNegative() {",
" Function<String,@Nullable String> removeA = a -> a.replace(\"a\",\"\");",
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment is still not addressed

akulk022 and others added 25 commits January 29, 2024 15:37
…terface"

This reverts commit 81f8b7d.

"Reverting EmptyLibraryModels changes"
…7' into library_models_Function_abhijitk7

# Conflicts:
#	nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java
#	nullaway/src/main/java/com/uber/nullaway/LibraryModels.java
#	nullaway/src/main/java/com/uber/nullaway/NullAway.java
#	nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
#	nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java
#	nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java
#	nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java
…7' into library_models_Function_abhijitk7

# Conflicts:
#	nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java
We should skip checking for errors here instead of crashing

(cherry picked from commit 091ac38)
Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Unfortunately something has gone wrong with this PR. It has removed changes that are present in the master branch and those removals are not showing up in the GitHub UI for some reason. Compare this branch's README.md Android section:

https://github.com/akulk022/NullAway/tree/library_models_Function_abhijitk7?tab=readme-ov-file#android

With the master branch:

https://github.com/uber/NullAway?tab=readme-ov-file#android

I think the best thing to do is to re-create this PR somehow, to make sure we're not losing master branch changes unintentionally.

@@ -145,10 +147,11 @@ private static boolean isClassFieldOfPrimitiveType(Symbol symbol) {
*
* @param symbol symbol for entity
* @param config NullAway config
* @param handler NullAway Handler
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @param handler NullAway Handler
* @param handler optional NullAway Handler

@@ -176,12 +179,15 @@ public boolean isSymbolUnannotated(Symbol symbol, Config config) {
* Check whether a class should be treated as nullness-annotated.
*
* @param classSymbol The symbol for the class to be checked
* @param config NullAway config
* @param handler NullAway handler
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @param handler NullAway handler
* @param handler optional NullAway handler

@@ -190,12 +196,15 @@ public boolean isClassNullAnnotated(Symbol.ClassSymbol classSymbol, Config confi
* <p>This method is recursive, using the cache on the way up and populating it on the way down.
*
* @param classSymbol The class to query, possibly an inner class
* @param config NullAway config
* @param handler NullAway handler
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* @param handler NullAway handler
* @param handler optional NullAway handler

@akulk022 akulk022 marked this pull request as draft February 1, 2024 00:49
@akulk022 akulk022 closed this Feb 1, 2024
msridhar added a commit that referenced this pull request Feb 9, 2024
…els (#903)

The below line previously reported an error considering the upperbound
of the generic type parameter couldn't be Nullable, adding it into
library models to allow it to be Nullable passes the test case.

```java
Function<String,@nullable String> removeA = a -> a.replace("a","");
```

We now have library model methods for treating a type as `@NullMarked`
and also for making the upper bound of type variables `@Nullable`. We
use these new methods to model `java.util.function.Function` as
`@NullMarked`, in JSpecify mode only.

We fix a related issue with unwanted errors when passing a lambda /
method reference to `@NullUnmarked` code; this was necessary to get
NullAway itself to still build with NullAway checking enabled.

(Due to issues with the branch this PR was recreated from the original
#893)

---------

Co-authored-by: Manu Sridharan <msridhar@gmail.com>
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.

None yet

3 participants