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

Make library models override annotations by default. #636

Merged
merged 3 commits into from
Aug 15, 2022

Conversation

lazaroclapp
Copy link
Collaborator

This change removes the short-lived AcknowledgeLibraryModelsOfAnnotatedCode
configuration flag.

It sets the behavior of NullAway to always allow library models to override
the nullability of annotated code, with an important caveat regarding #445:

Whenever we have a method D.m which overrides a method C.m for which we
have a library model, NullAway's behavior depends on whether D.m is an
annotated or unannotated method:

  1. If D.m is within unannotated / third-party code, we consider the library
    model of C.m to extend to D.m (and to all such third-party overriding
    methods of C.m). This minimizes redundancy in our library models and makes
    them more robust to the changes within third-party library internals.
  2. However, for an overriding D.m within annotated code, we don't propagate
    the library model, requiring either D.m to be annotated in a compatible
    manner or else a separate library model for it.

This makes it so that libraries for which the user doesn't have the source code
(i.e. can't add annotations), but which they wish to add to the AnnotatedPackages
list, require more comprehensive library models than "unannotated" libraries.
However, this is fully consistent with the intent behind marking code as annotated,
which is stricter checking of its nullability.

This change removes the short-lived `AcknowledgeLibraryModelsOfAnnotatedCode`
configuration flag.

It sets the behavior of NullAway to always allow library models to override
the nullability of annotated code, with an important caveat regarding #445:

Whenever we have a method `D.m` which overrides a method `C.m` for which we
have a library model, NullAway's behavior depends on whether `D.m` is an
annotated or unannotated method:

1) If `D.m` is within unannotated / third-party code, we consider the library
   model of `C.m` to extend to `D.m` (and to all such third-party overriding
   methods of `C.m`). This minimizes redundancy in our library models and makes
   them more robust to the changes within third-party library internals.
2) However, for an overriding `D.m` within annotated code, we don't propagate
   the library model, requiring either `D.m` to be annotated in a compatible
   manner or else a separate library model for it.

This makes it so that libraries for which the user doesn't have the source code
(i.e. can't add annotations), but which they wish to add to the `AnnotatedPackages`
list, require more comprehensive library models than "unannotated" libraries.
However, this is fully consistent with the intent behind marking code as annotated,
which is stricter checking of its nullability.
@lazaroclapp
Copy link
Collaborator Author

Didn't recall the entire discussion when I made this change, but this ended up being solution (3) from this comment: #453 (comment)

I wonder if it's still worth it to take a step back and implement option (2) instead, but (3) seems to handle most use cases.

@coveralls
Copy link

coveralls commented Aug 9, 2022

Pull Request Test Coverage Report for Build #905

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • 12 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.006%) to 92.523%

Files with Coverage Reduction New Missed Lines %
../nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java 3 99.39%
../nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java 4 94.12%
../nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java 5 93.83%
Totals Coverage Status
Change from base Build #902: -0.006%
Covered Lines: 4801
Relevant Lines: 5189

💛 - Coveralls

@msridhar
Copy link
Collaborator

msridhar commented Aug 9, 2022

I wonder if it's still worth it to take a step back and implement option (2) instead, but (3) seems to handle most use cases.

I think (3) is better for now. I think we should avoid the complexity of introducing a first-party distinction if we can avoid it.

@msridhar
Copy link
Collaborator

msridhar commented Aug 9, 2022

Whenever we have a method D.m which overrides a method C.m for which we have a library model, NullAway's behavior depends on whether D.m is an annotated or unannotated method:

  1. If D.m is within unannotated / third-party code, we consider the library
    model of C.m to extend to D.m (and to all such third-party overriding
    methods of C.m). This minimizes redundancy in our library models and makes
    them more robust to the changes within third-party library internals.
  2. However, for an overriding D.m within annotated code, we don't propagate
    the library model, requiring either D.m to be annotated in a compatible
    manner or else a separate library model for it.

Do we already have tests for this somewhere? I'm particularly interested in case 2 and making sure there are override errors for D.m if it is not annotated in a manner compatible with the library model. But ideally we'd have tests for both.

@lazaroclapp
Copy link
Collaborator Author

lazaroclapp commented Aug 9, 2022

Do we already have tests for this somewhere? I'm particularly interested in case 2 and making sure there are override errors for D.m if it is not annotated in a manner compatible with the library model. But ideally we'd have tests for both.

Yes we do!

In particular, it was this test that reminded me to not just undo #445 by switching the default in the obvious way: NullAwayFrameworkTests.overridingNativeModelsInAnnotatedCodeDoesNotPropagateTheModel

For the other case, the tests introduced with the flag cover it, and we just removed the flag but kept the =true test in this PR.

@msridhar
Copy link
Collaborator

msridhar commented Aug 9, 2022

Thanks! Here's another case. I added this test to NullAwayGuavaParametricNullnessTests in my local copy of the branch (sorry for formatting, was hacking it up quickly):

  @Test
  public void testFunctionMethodOverride() {
    defaultCompilationHelper
            .addSourceLines(
                    "Test.java",
                    "package com.uber;",
                    "import com.google.common.base.Function;",
                    "import javax.annotation.Nullable;",
                    "class Test {",
                    "    public static void testFoo() {",
                    "    Function<Object, Object> f = new Function<Object, Object>() {\n" +
                            "      @Override\n" +
                            "      @Nullable\n" +
                            "      public Object apply(Object input) {\n" +
                            "        return null;\n" +
                            "      }\n" +
                            "    };",
                    "    }",
                    "}"
            ).doTest();
  }

Our built-in library models list com.google.common.base.Function.apply() as having a non-null parameter and a non-null return. But with this test, I get this error:

/Test.java:9: warning: [NullAway] parameter input is @NonNull, but parameter in superclass method com.google.common.base.Function.apply(F) is @Nullable
      public Object apply(Object input) {

I was expecting to get an override error about the return type, not the parameter type. Is this expected behavior? Or is this due to a weird interaction with our @ParametricNullness support?

@lazaroclapp
Copy link
Collaborator Author

I was expecting to get an override error about the return type, not the parameter type. Is this expected behavior? Or is this due to a weird interaction with our @ParametricNullness support?

  1. Good catch! (Although, just to be clear, the above example will also be a bug in current master and in 0.9.9/0.9.8 with Guava in AnnotatedPackages, it has nothing to do with @ParametricNullness, we just generally ignore library models on parameters for annotated code. This is a bug, but not a new one.)
  2. The fix will have to be slightly involved, since we kind of designed our extension points around the whole annotated vs unannotated divide (it's not just internal to LibraryModelsHandler, which is why I missed it when implementing AllowLibraryModelsOverrideAnnotations).

Basically, we have the following two methods which are used for parameter override checking of unannotated code:

  • Handler.onUnannotatedInvocationGetNonNullPositions(...)
  • Handler.onUnannotatedInvocationGetExplicitlyNullablePositions(...)

Key word being Unannotated. Those methods don't even get called for annotated code, and it's not just a matter of renaming them and removing the restriction either, because different handlers use them expecting different scenarios (such as RestrictiveAnnotationHandler, which we don't want suddenly operating on annotated code).

I think the way to move forward here is to simplify those two unrelated chains of callbacks, plus the code that handles the "annotated" case, to a slightly more general Handler.onInvocationUpdateArgumentNullness(...) which takes the map of argIdx to nullability before each handler, and returns the map as updated by that handler. Then we can just call that after either reading the annotations (for annotated code) or just passing it our optimistic defaults. Each handler can decide whether it only cares about unannotated code or if cares either way.

One question is whether that should be this same PR or a new one?

@msridhar
Copy link
Collaborator

Yes, this logic is all getting a bit complicated 😅 I think maybe we should try to write some documentation again on how all these features (annotated packages, library models, flags) interact.

I think it's better to address the latest bug in a separate PR as it is in some sense unrelated to this pull request. I'll review the code in this PR soon.

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.

LGTM with one small tweak to a comment

Comment on lines 110 to 111
// When looking up library models of annotated code, we match the exact method signature only,
// overriding implementations that share the same model must be explicitly given their own
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
// When looking up library models of annotated code, we match the exact method signature only,
// overriding implementations that share the same model must be explicitly given their own
// When looking up library models of annotated code, we match the exact method signature only;
// overriding methods in subclasses must be explicitly given their own

@lazaroclapp lazaroclapp merged commit c2de997 into master Aug 15, 2022
@lazaroclapp lazaroclapp deleted the lazaro_flip_default_library_models_annotated_code branch August 15, 2022 19:14
lazaroclapp added a commit that referenced this pull request Aug 24, 2022
This is a follow up to changes in #639, #636, and #624. Here we
generalize the former Handler callback:

- onUnannotatedInvocationGetExplicitlyNonNullReturn

which was used to change the nullability of method returns in
unannotated code for the purpose of method `@Override` checks
(i.e. subtyping).

We provide instead:

- onOverrideMethodInvocationReturnNullability

Which allows handlers to change the nullability of method
returns for both annotated and unannotated code, for the same
purposes.

Additionally, we implement this new handler callback in
`LibraryModelsHandler`.

Before this change, marking a return as non-null in a library model
affected checking of method invocations, but not necessarily
overriding/subtyping checks.
lazaroclapp added a commit that referenced this pull request Aug 29, 2022
This is a follow up to changes in #639, #636, and #624. Here we
generalize the former Handler callback:

- onUnannotatedInvocationGetExplicitlyNonNullReturn

which was used to change the nullability of method returns in
unannotated code for the purpose of method `@Override` checks
(i.e. subtyping).

We provide instead:

- onOverrideMethodInvocationReturnNullability

Which allows handlers to change the nullability of method
returns for both annotated and unannotated code, for the same
purposes.

Additionally, we implement this new handler callback in
`LibraryModelsHandler`.

Before this change, marking a return as non-null in a library model
affected checking of method invocations, but not necessarily
overriding/subtyping checks.
lazaroclapp added a commit that referenced this pull request Aug 30, 2022
…#641)

This is a follow up to changes in #639, #636, and #624. Here we
generalize the former Handler callback:

- onUnannotatedInvocationGetExplicitlyNonNullReturn

which was used to change the nullability of method returns in
unannotated code for the purpose of method `@Override` checks
(i.e. subtyping).
    
We provide instead:
    
- onOverrideMethodInvocationReturnNullability
    
Which allows handlers to change the nullability of method
returns for both annotated and unannotated code, for the same
purposes.
    
Additionally, we implement this new handler callback in
`LibraryModelsHandler`.
    
Before this change, marking a return as non-null in a library model
affected checking of method invocations, but not necessarily
overriding/subtyping checks.
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.

3 participants