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

Support unique classes (Fixes #3313) #3336

Merged
merged 35 commits into from Jul 21, 2020

Conversation

aditya3434
Copy link
Contributor

@aditya3434 aditya3434 commented May 28, 2020

The Aliasing Checker gives "unique.leaked" for local variables only when they are explicitly annotated. This is because of the calling of hasExplicitAnnotation() type method in AliasingVisitor.java, in the canbeLeaked() method. By annotating class Data with @unique, the local variables start as @MaybeAliased but eventually get refined to @unique, due to the upper bound. The class objects are unique but since they are not explicitly annotated, they bypass the canbeLeaked() method and don't report errors. Removing this explicit requirement fixes the issue, and gives the expected output without giving any side effects (under my testing).

This PR should be merged along with the annotated JDK pull request.
(typetools/jdk#57)

Fixes #3313.

@@ -271,7 +271,13 @@ private boolean canBeLeaked(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
boolean isMethodInvocation = exp.getKind() == Kind.METHOD_INVOCATION;
boolean isNewClass = exp.getKind() == Kind.NEW_CLASS;
return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isArray = type.getUnderlyingType().getKind().toString().equalsIgnoreCase("array");
Copy link
Member

Choose a reason for hiding this comment

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

You should try to avoid operations on strings. Is there no TypeKind to compare against?

Copy link
Member

Choose a reason for hiding this comment

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

Ping.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have used your suggested logic in my latest commit. There is no need for array TypeKind comparison in the code.

return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isArray = type.getUnderlyingType().getKind().toString().equalsIgnoreCase("array");
boolean isNull = exp.getKind() == Kind.NULL_LITERAL;
if (type.getUnderlyingType().toString().startsWith("java.lang") || isArray || isNull) {
Copy link
Member

Choose a reason for hiding this comment

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

What is the motivation for this special logic? You should explain the logic here or at least in the PR description.

Copy link
Member

Choose a reason for hiding this comment

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

Also, do add tests that cover such special logic, in at least a few important cases.

Copy link
Contributor Author

@aditya3434 aditya3434 Jun 6, 2020

Choose a reason for hiding this comment

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

The main logic I was following that the Aliasing Checker doesn't check for explicit annotations for user defined classes. So only those classes that the user has specifically annotated as @unique would construct @unique classes. This would also prevent all trivial objects like strings, int, object, float, etc. from always becoming @unique (it is more convenient to explicitly annotate them).

However, I couldn't figure out how to make the AliasingVisitor check whether a class was user-defined or not. So, instead I listed classes and data types for which one would need explicit annotations. These included java.lang classes, arrays and null values. All this complicated conditions could be avoided if the Visitor could check for user-defined classes.

if (annotated class is user defined) {
return type.hasAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
}
else {
return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
}

Copy link
Member

Choose a reason for hiding this comment

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

Can you clarify what you mean with "user defined"?
Why should the handling of explicit annotations depend on whether the class is "user defined"?

Copy link
Contributor Author

@aditya3434 aditya3434 Jun 8, 2020

Choose a reason for hiding this comment

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

In the stub file stubfile.astub in tests/aliasing, Object and String classes are annotated as @unique by default. So, if I don't check for explicit annotations for all classes, then, even trivial operations like string assignments give a "unique.leaked" error in the test cases.

// part of TypeRefinementTest.java in tests/aliasing
void rule1() {
String unique = new String();
isUnique(unique);
String notUnique = unique; // gives unique.leaked error even though @unique is never used
}

So I decided to limit this checking for "user-defined" classes (classes that are not part of any in-built java library, like java.lang or java.util). For example, in the code in the feature request #3313 (#3313), class Data and Demo are user-defined classes, and since Data is annotated as @unique, all its instances don't require explicit annotations to be unique as well.

Copy link
Member

Choose a reason for hiding this comment

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

We do not want different behavior for built-in classes and user-defined classes. The same logic should be applied to all source code, regardless of who wrote the code.

There is a difference between classes String and Data in these two examples.
In String the default constructor is annotated as returning a unique object. The class declaration is not annotated. Therefore, it is fine in the test case to have a non-unique String reference.
On the other hand class Data in this new test case is annotated as @Unique requiring that only @Unique references exist.
So your check should be for that difference between the two classes.

Also can you move the annotations from framework/tests/aliasing/stubfile.astub into typetools/jdk#52?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added a new method for the above requested logic. Now the code doesn't need to deal with the special cases of arrays, null and java.lang classes. I have used the getDeclAnnotations method to get the set of declared annotations of the class as a whole, and then searched for the unique annotation mirror in the set. The code now gives leaked errors without explicit annotations only when the class is annotated as @unique. Annotating just the constructor will allow non-unique object references.

class Demo {
void check(Data p) { // p is @Unique Data Object
// :: error: (unique.leaked)
Data y = p; // @Unique p is leaked
Copy link
Member

Choose a reason for hiding this comment

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

Is the type of the left-hand side important? Can you add another method that does

Object check2(Data p) { return p; }

which I guess should also give a unique.leaked error.

Copy link
Member

Choose a reason for hiding this comment

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

Ping.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, the type of the left-hand side isn't important. However, the method doesn't give an error, even when Data is explicitly annotated as @unique.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure how you mean this. Does Object z = p; not raise an error? Shouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Unique
class Data {

    Object check2(Data p) { return p; }

    void check(Data p) {
        Object z = p;
    }
}

In the code given above, Object z = p; does raise an error. Hence, the left hand side isn't important. However, the check2 function (where you are returning a @unique Data as an Object) doesn't raise an error both in the typetools/master and in my modified code, even if the Data p is explicitly annotated as @unique Data p.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, can you expand this test case with the Object local variable and the expected error.
Then file an issue that shows the problem with the return version.

@wmdietl wmdietl assigned aditya3434 and unassigned wmdietl Jun 5, 2020
@@ -271,7 +271,13 @@ private boolean canBeLeaked(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
boolean isMethodInvocation = exp.getKind() == Kind.METHOD_INVOCATION;
boolean isNewClass = exp.getKind() == Kind.NEW_CLASS;
return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isArray = type.getUnderlyingType().getKind().toString().equalsIgnoreCase("array");
Copy link
Member

Choose a reason for hiding this comment

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

Ping.

return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isArray = type.getUnderlyingType().getKind().toString().equalsIgnoreCase("array");
boolean isNull = exp.getKind() == Kind.NULL_LITERAL;
if (type.getUnderlyingType().toString().startsWith("java.lang") || isArray || isNull) {
Copy link
Member

Choose a reason for hiding this comment

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

We do not want different behavior for built-in classes and user-defined classes. The same logic should be applied to all source code, regardless of who wrote the code.

There is a difference between classes String and Data in these two examples.
In String the default constructor is annotated as returning a unique object. The class declaration is not annotated. Therefore, it is fine in the test case to have a non-unique String reference.
On the other hand class Data in this new test case is annotated as @Unique requiring that only @Unique references exist.
So your check should be for that difference between the two classes.

Also can you move the annotations from framework/tests/aliasing/stubfile.astub into typetools/jdk#52?

class Demo {
void check(Data p) { // p is @Unique Data Object
// :: error: (unique.leaked)
Data y = p; // @Unique p is leaked
Copy link
Member

Choose a reason for hiding this comment

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

Ping.

import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.ThrowTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.*;
Copy link
Member

Choose a reason for hiding this comment

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

Do not star import.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of star import, I listed the library data types that were required in my latest commit

* @return boolean true if class if unique and false otherwise
*/
private boolean isUniqueClass(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
Copy link
Member

Choose a reason for hiding this comment

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

The caller already computed type and you don't seem to use exp in this method. So why not just pass the type as parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I replaced exp with type as an argument in the isUniqueClass in my latest commit

private boolean isUniqueClass(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
Element el = types.asElement(type.getUnderlyingType());
if (el != null) {
Copy link
Member

Choose a reason for hiding this comment

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

It might be easier as if (el == null) { return false; } and then the same for annoMirrors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have separated out el and annoMirrors checks instead of the nested if conditions. If either of them is null, the method returns false

if (mirror.getAnnotationType()
.asElement()
.getSimpleName()
.contentEquals("Unique")) {
Copy link
Member

Choose a reason for hiding this comment

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

Again, don't use Strings for things like this. Can you use AnnotationUtils.containsSameByClass https://github.com/typetools/checker-framework/blob/master/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java#L230 ? Then you don't need the for loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have used the AnnotationUtils.containtsSameByClass method in my latest commit

class Demo {
void check(Data p) { // p is @Unique Data Object
// :: error: (unique.leaked)
Data y = p; // @Unique p is leaked
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure how you mean this. Does Object z = p; not raise an error? Shouldn't it?

return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isUniqueClassFlag = isUniqueClass(exp);
if (isUniqueClassFlag) {
return type.hasAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need the type.hasAnnotation again, if you already know that isUniqueClass?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I see that was a redundant check. I have not used it in my latest commits

@@ -271,7 +265,37 @@ private boolean canBeLeaked(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
boolean isMethodInvocation = exp.getKind() == Kind.METHOD_INVOCATION;
boolean isNewClass = exp.getKind() == Kind.NEW_CLASS;
return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isUniqueClassFlag = isUniqueClass(exp);
Copy link
Member

Choose a reason for hiding this comment

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

Remove the Flag part. Notice the style for the other is variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have removed the flag part from the variable name in my latest commit

@@ -271,7 +265,37 @@ private boolean canBeLeaked(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
boolean isMethodInvocation = exp.getKind() == Kind.METHOD_INVOCATION;
boolean isNewClass = exp.getKind() == Kind.NEW_CLASS;
return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isUniqueClassFlag = isUniqueClass(exp);
if (isUniqueClassFlag) {
Copy link
Member

Choose a reason for hiding this comment

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

Could this just be return !isMethodInvocation && !isNewClass && (isUniqueClass || type.hasExplicitAnnotation(Unique.class));

Copy link
Member

Choose a reason for hiding this comment

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

Maybe extract the (isUniqueClass || type.hasExplicitAnnotation(Unique.class) into a separate local and document why that is the logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have separated the (isUniqueClass || type.hasExplicitAnnotation(Unique.class) into a separate local variable and documented the logic.

@@ -271,7 +265,37 @@ private boolean canBeLeaked(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
Copy link
Member

Choose a reason for hiding this comment

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

You should also update the javadoc with the change in behavior.
Do you understand what canBeLeaked expresses? The documentation is rather unclear.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have changed the documentation for the canBeLeaked method in my latest commit

@wmdietl wmdietl assigned aditya3434 and unassigned wmdietl Jun 25, 2020
@smillst
Copy link
Member

smillst commented Jun 30, 2020

@aditya3434 Is this ready to review again? If so, please remove your assignment and assign @wmdietl. Thanks!

@aditya3434
Copy link
Contributor Author

@aditya3434 Is this ready to review again? If so, please remove your assignment and assign @wmdietl. Thanks!

Thanks for the heads up! I have unassigned myself and assigned @wmdietl for the review.

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import java.util.Set;
import javax.lang.model.element.*;
Copy link
Member

Choose a reason for hiding this comment

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

Do not use wildcard imports.

Copy link
Member

Choose a reason for hiding this comment

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

We had this before: #3336 (comment)
Look how to change your IDE configuration to not make such changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have changed the import statement in my latest commit. Turns out my IDE automatically * imports whenever it imports more than 3 packages.

}

/**
* Returns true if class of annotated type {@code type} has annotation {@code @Unique}
Copy link
Member

Choose a reason for hiding this comment

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

The summary should end with a period.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe use: Return true if the class declaration for annotated type ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have adjusted the summary in my latest commit

*
* @param exp the Tree to check
*/
private boolean canBeLeaked(Tree exp) {
AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(exp);
boolean isMethodInvocation = exp.getKind() == Kind.METHOD_INVOCATION;
boolean isNewClass = exp.getKind() == Kind.NEW_CLASS;
return type.hasExplicitAnnotation(Unique.class) && !isMethodInvocation && !isNewClass;
boolean isUniqueType = (isUniqueClass(type) || type.hasExplicitAnnotation(Unique.class));
Copy link
Member

Choose a reason for hiding this comment

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

The outer parenthesis aren't necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have removed the parenthesis in my latest commit.

class Demo {
void check(Data p) { // p is @Unique Data Object
// :: error: (unique.leaked)
Data y = p; // @Unique p is leaked
Copy link
Member

Choose a reason for hiding this comment

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

Ok, can you expand this test case with the Object local variable and the expected error.
Then file an issue that shows the problem with the return version.

@wmdietl wmdietl assigned aditya3434 and unassigned wmdietl Jul 20, 2020
@aditya3434
Copy link
Contributor Author

I have added the Object local variable error in the test case and have filed an issue with the return version.

Issue Link : #3486

@aditya3434 aditya3434 assigned wmdietl and unassigned aditya3434 Jul 21, 2020
@wmdietl wmdietl changed the title 3313 explicit annotation fix Support unique classes (Fixes #3313) Jul 21, 2020
Copy link
Member

@wmdietl wmdietl left a comment

Choose a reason for hiding this comment

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

Thanks, this looks good now!

@wmdietl wmdietl merged commit c44ded4 into typetools:master Jul 21, 2020
@aditya3434 aditya3434 deleted the 3313-explicit-annotation-fix branch July 27, 2020 07:54
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.

Feature request: @AlwaysUnique (?) to mark classes whose instances must always be @Unique
4 participants