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
Support unique classes (Fixes #3313) #3336
Conversation
This reverts commit fd536c4.
@@ -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"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…tya3434/checker-framework into 3313-explicit-annotation-fix
This reverts commit fd536c4.
@@ -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"); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.*; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not star import.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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")) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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));
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@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.*; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
I have added the Issue Link : #3486 |
There was a problem hiding this 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!
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.