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

External Library Models Integration #922

Merged
merged 61 commits into from
Apr 9, 2024

Conversation

akulk022
Copy link
Collaborator

@akulk022 akulk022 commented Feb 24, 2024

The newly added library-model module consists of a CLI process that takes an input directory with annotated java source files as a command line parameter and uses com.github.javaparser APIS to generate libmodels.astubx file containing method stubs for methods that return @nullable. This can be run using the existing JarInferEnabled and JarInferUseReturnAnnotations flags.

This allows us to be able catch issues as shown in the below example from externally annotated source code:

@NullMarked
public class AnnotationExample {
    @Nullable
    public String makeUpperCase(String inputString) {
        if (inputString == null || inputString.isEmpty()) {
            return null;
        } else {
            return inputString.toUpperCase();
        }
    }
}
class Test {
    static AnnotationExample annotationExample = new AnnotationExample();
    static void test(String value){}
    static void testPositive() {
        // BUG: Diagnostic contains: passing @Nullable parameter 'annotationExample.makeUpperCase(\"nullaway\")'
        test(annotationExample.makeUpperCase(\"nullaway\"));
    }    
}

Copy link

codecov bot commented Feb 26, 2024

Codecov Report

Attention: Patch coverage is 9.70874% with 93 lines in your changes are missing coverage. Please review.

Project coverage is 86.08%. Comparing base (5acd394) to head (80be0d4).

Files Patch % Lines
.../uber/nullaway/libmodel/LibraryModelGenerator.java 0.00% 92 Missing ⚠️
...er/nullaway/handlers/InferredJARModelsHandler.java 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #922      +/-   ##
============================================
- Coverage     87.15%   86.08%   -1.07%     
- Complexity     2011     2018       +7     
============================================
  Files            78       79       +1     
  Lines          6522     6612      +90     
  Branches       1265     1280      +15     
============================================
+ Hits           5684     5692       +8     
- Misses          422      510      +88     
+ Partials        416      410       -6     

☔ 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.

A few quick comments to start

@akulk022 akulk022 requested a review from msridhar March 6, 2024 00:02
@msridhar
Copy link
Collaborator

msridhar commented Mar 6, 2024

At a high level I'd like to do some renaming of the new modules. Let's use library-model instead of lib-model, consistently. Also:

  • lib-model-consume -> library-model-generator
  • lib-model-consume-cli -> library-model-generator-cli
  • lib-model-test -> library-model-generator-integration-test

@msridhar msridhar marked this pull request as ready for review March 14, 2024 04:19
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.

Overall this looks great now! I have one more comment which should be an easy fix. I think the remaining high-level question, for me, is whether we should rename classes related to JarInfer inside NullAway (since they now serve a more general purpose), and whether we should add extra command-line flags instead of just having JarInferEnabled. I think we can fix those things in a follow-up PR but we should decide what we want to do.

Otherwise, I'm going to ask @lazaroclapp to review this PR as he originally wrote the stubx generation code.

Comment on lines +192 to +194
if (a.getNameAsString().equalsIgnoreCase(NULL_MARKED)) {
this.isNullMarked = true;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This logic won't handle cases where @NullMarked appears on some nested classes but not others. But, I think for now, we can just say we only support @NullMarked on the top-level class. Can we add a comment to this effect?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It also needs @NullMarked on the same file, correct? We don't have a way to add this to the package somewhere and have the rest of the traversal be aware of that (that seems more likely than mixed marked/unmarked code in the models, but I am not sure)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, you're correct, @lazaroclapp, we are assuming an explicit @NullMarked on the top-level class in a source file, and that there is only one top-level class per source file. We won't find anything on package-info.java or module-info.java for now. Both these assumptions hold for jspecify/jdk

Copy link
Collaborator

Choose a reason for hiding this comment

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

@akulk022 can you update the comment in light of the above?

@msridhar msridhar requested a review from lazaroclapp March 19, 2024 16:13
Copy link
Collaborator

@lazaroclapp lazaroclapp left a comment

Choose a reason for hiding this comment

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

Did a pass. Had a bunch of comments, but they are mostly questions and notes for the future. Overall this looks good to me.

build.gradle Outdated Show resolved Hide resolved
import com.google.common.collect.ImmutableSet;

/** A record describing the annotations associated with a java method and its arguments. */
@AutoValue
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we otherwise using @AutoValue for something or what's the advantage of it here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry this is my fault. I thought this was new code and thought that @AutoValue made it a bit cleaner. I didn't realize it had been moved from somewhere else. @lazaroclapp would you like us to undo the @AutoValue-ization of this file?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Never mind, seems we have been using @AutoValue in at least some parts of the codebase, since the beginning, so I am fine having it here too, at least until we can do JDK 17 records.

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2017. Uber Technologies
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not that I care about this anymore, but I suspect Uber would want these copyright headers to be updated for the right years 😉 (e.g. 2024 here and 20[...]-2024 for the files that were altered, I think)

*/
public static void main(String[] args) {
LibraryModelGenerator libraryModelGenerator = new LibraryModelGenerator();
libraryModelGenerator.generateAstubxForLibraryModels(args[0], args[1]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Check the number of args and print a basic usage message? (Not sure if we do that for JarInfer, and I don't think we need a full argument parser here yet, a check on the length of args would suffice for now)

Comment on lines +30 to +31
"-XepOpt:NullAway:JarInferEnabled=true",
"-XepOpt:NullAway:JarInferUseReturnAnnotations=true"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with @yuxincs that overloading the meaning of the JarInfer[...] CLI options for NullAway for this is bad practice, specially because other than the Android SDK, JarInfer no longer uses .astubx but actually modifies the bytecode of jars to add annotations (so this options aren't used with JarInfer except for that Android SDK jar).

It can certainly be a follow up PR, but I'd be in favor of rethinking these CLI flags. Maybe -XepOpt:NullAway:JarInferEnabled is actually something like -XepOpt:NullAway:ScanClassPathForAstubXModels? Is there a better name? A nicer way to specify which .astubx files we want to load without breaking things in our build internally? (I think there is! We only really care about that Android SDK astubx file right now, so it's not like this changes for each target like it used to, cc: @yuxincs )

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am in favor of changing these, but maybe in a follow-up PR. I don't think we need to go as far as keeping the old flag but deprecating it, as long as we document properly in release notes. But we should probably check as to what this breaks (if anything) internally at Uber.

@akulk022 can you open an issue on renaming these flags?

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've created the issue #940

Comment on lines +271 to +274
return (annotation.getNameAsString().equalsIgnoreCase(NULLABLE)
&& this.isJspecifyNullableImportPresent)
|| annotation.getNameAsString().equalsIgnoreCase(JSPECIFY_NULLABLE_IMPORT);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we care about star imports or other nonsense or is it fine here to only count either FQN or an import of the FQN?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this logic is fine but we should add a comment that we don't consider star imports @akulk022

Comment on lines 109 to 112
Map<String, String> importedAnnotations =
ImmutableMap.of(
"Nonnull", "javax.annotation.Nonnull",
"Nullable", "javax.annotation.Nullable");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are we using the javax annotations here and not JSpecify? Some limitation with StubxWriter?

Copy link
Collaborator Author

@akulk022 akulk022 Mar 20, 2024

Choose a reason for hiding this comment

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

if (methodAnnotations.contains("javax.annotation.Nullable")) {

No limitation with StubxWriter 🙂, just used it because the check here considers the javax annotation and I probably didn't consider adding an OR and including the jspecify annotation in the condition. I've updated it.

Comment on lines +192 to +194
if (a.getNameAsString().equalsIgnoreCase(NULL_MARKED)) {
this.isNullMarked = true;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

It also needs @NullMarked on the same file, correct? We don't have a way to add this to the package somewhere and have the rest of the traversal be aware of that (that seems more likely than mixed marked/unmarked code in the models, but I am not sure)

String parentClassName = "";
if (parentClassNode.isPresent()) {
if (parentClassNode.get() instanceof ClassOrInterfaceDeclaration) {
parentClassName = ((ClassOrInterfaceDeclaration) parentClassNode.get()).getNameAsString();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is just the simple name, right? So currently this breaks when dealing with nested/inner classes? i.e. the concatenation below:

            packageName
                + "."
                + parentClassName
                + ":"
                + getMethodReturnTypeString(md)
                [...]

Ends up producing com.example.Bar:String baz() for:

@NullMarked
class Foo {
   public class Bar {
      @Nullable public String baz() {...}
   }
}

correct?

Copy link
Collaborator Author

@akulk022 akulk022 Mar 21, 2024

Choose a reason for hiding this comment

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

Yes, that is correct. I'll look into making this work.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@akulk022 is this easy to fix? Or should it wait for a follow up?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@msridhar I'm working on a fix since it seems like it should be easy enough.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@akulk022 be sure to add a test for it as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@msridhar @lazaroclapp I've updated the logic to handle this scenario, I've also added a test for it. Does this work and are there more scenarios I should test?

*/
private String getMethodReturnTypeString(MethodDeclaration md) {
if (md.getType() instanceof ClassOrInterfaceType) {
return md.getType().getChildNodes().get(0).toString();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that using simple names is something we inherited from the CF stub format + us not explicitly handling imports, I think. So we might want to change these to FQNs at some point (but probably not in this PR)

@akulk022 akulk022 requested a review from lazaroclapp March 24, 2024 21:07
@@ -157,7 +155,7 @@ public Result process(Path localPath, Path absolutePath, ParseResult<Compilation

private static class AnnotationCollectionVisitor extends VoidVisitorAdapter<Void> {

private String packageName = "";
private StringBuilder parentName;
Copy link
Collaborator

@msridhar msridhar Mar 27, 2024

Choose a reason for hiding this comment

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

Probably better to just make this a String? I think making it a StringBuilder is an unnecessary optimization and makes the code more brittle. You can just overwrite it with new String values and I think it'll be fine

Copy link
Collaborator

@lazaroclapp lazaroclapp left a comment

Choose a reason for hiding this comment

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

LGTM! Definitely some potential future improvements are possible (and renaming some stuff so it's less tied to JarInfer), but as far as initial support for building JSpecify JDK astubx files, this is excellent!

@msridhar
Copy link
Collaborator

msridhar commented Apr 9, 2024

Going to merge this now, and we will build on it in future PRs

@msridhar msridhar merged commit 09db47a into uber:master Apr 9, 2024
10 of 12 checks passed
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