Skip to content

Commit 69e9815

Browse files
committed
Java: Added new query java/visible-for-testing-abuse
1 parent 1fab97b commit 69e9815

File tree

10 files changed

+231
-0
lines changed

10 files changed

+231
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# J-T-003: Accessing any method, field or class annotated with `@VisibleForTesting` from production code is discouraged
2+
3+
Accessing class members annotated with `@VisibleForTesting` from production code goes against the intention of the annotation and may indicate programmer error.
4+
5+
## Overview
6+
7+
The `@VisibleForTesting` serves to increase visibility of methods, fields or classes for the purposes of testing. Accessing methods, fields or classes that are annotated with `@VisibleForTesting` in production code (not test code) abuses the intention of the annotation.
8+
9+
## Recommendation
10+
11+
Only access methods, fields or classes annotated with `@VisibleForTesting` from test code. If the visibility of the methods, fields or classes should generally be relaxed, use Java language access modifiers.
12+
13+
## Example
14+
15+
```java
16+
public class Annotated {
17+
@VisibleForTesting static int f(){}
18+
}
19+
20+
/* src/test/java/Test.java */
21+
int i = Annotated.f(); // COMPLIANT
22+
23+
/* src/main/Source.java */
24+
int i = Annotated.f(); // NON_COMPLIANT
25+
26+
```
27+
28+
## Implementation notes
29+
30+
This rule alerts on any implementation of the annotation `VisibleForTesting`, regardless of where it is provided from.
31+
32+
The rule also uses the following logic to determine what an abuse of the annotation is:
33+
34+
1) If public or protected member/type is annotated with `VisibleForTesting`, it's assumed that package-private access is enough for production code. Therefore the rule alerts when a public or protected member/type annotated with `VisibleForTesting` is used outside of its declaring package.
35+
2) If package-private member/type is annotated with `VisibleForTesting`, it's assumed that private access is enough for production code. Therefore the rule alerts when a package-private member/type annotated with `VisibleForTesting` is used outside its declaring class.
36+
37+
## References
38+
- Example Specific Implementation of a VisibleForTesting Annotation: [AssertJ VisibleForTesting](https://javadoc.io/doc/org.assertj/assertj-core/latest/org/assertj/core/util/VisibleForTesting.html)
39+
- Assumptions of what level of access is permittable for each access modifier and the annotation: [JetBrains VisibleForTesting](https://javadoc.io/doc/org.jetbrains/annotations/22.0.0/org/jetbrains/annotations/VisibleForTesting.html)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @id java/visible-for-testing-abuse
3+
* @name Accessing any method, field or class annotated with `@VisibleForTesting` from production code is discouraged
4+
* @description Accessing any method, field or class annotated with `@VisibleForTesting` from
5+
* production code goes against the intention of the annotation and may indicate
6+
* programmer error.
7+
* @kind problem
8+
* @precision high
9+
* @problem.severity warning
10+
* @tags maintainability
11+
* readability
12+
*/
13+
14+
import java
15+
16+
/**
17+
* A `Callable` is within some `RefType`
18+
*/
19+
predicate isWithinType(Callable c, RefType t) { c.getDeclaringType() = t }
20+
21+
/**
22+
* A `Callable` is within same package as the `RefType`
23+
*/
24+
predicate isWithinPackage(Callable c, RefType t) {
25+
c.getDeclaringType().getPackage() = t.getPackage()
26+
}
27+
28+
predicate withinStaticContext(NestedClass c) {
29+
c.isStatic() or
30+
c.(AnonymousClass).getClassInstanceExpr().getEnclosingCallable().isStatic() // JLS 15.9.2
31+
}
32+
33+
RefType enclosingInstanceType(Class inner) {
34+
not withinStaticContext(inner) and
35+
result = inner.(NestedClass).getEnclosingType()
36+
}
37+
38+
class OuterClass extends Class {
39+
OuterClass() { this = enclosingInstanceType+(_) }
40+
}
41+
42+
/**
43+
* An innerclass is accessed outside of its outerclass
44+
* and also outside of its fellow inner parallel classes
45+
*/
46+
predicate isWithinDirectOuterClassOrSiblingInner(
47+
Callable classInstanceEnclosing, RefType typeBeingConstructed
48+
) {
49+
exists(NestedClass inner, OuterClass outer |
50+
outer = enclosingInstanceType(inner) and
51+
typeBeingConstructed = inner and
52+
// where the inner is called from the outer class
53+
classInstanceEnclosing.getDeclaringType() = outer
54+
)
55+
or
56+
// and inner is called from the a parallel inner
57+
exists(NestedClass inner, OuterClass outer, NestedClass otherinner |
58+
typeBeingConstructed = inner and
59+
outer = enclosingInstanceType(otherinner) and
60+
outer = enclosingInstanceType(inner) and
61+
classInstanceEnclosing.getDeclaringType() = otherinner
62+
)
63+
}
64+
65+
from Annotatable annotated, Annotation annotation, Expr e
66+
where
67+
annotation.getType().hasName("VisibleForTesting") and
68+
annotated.getAnAnnotation() = annotation and
69+
(
70+
// field access
71+
exists(FieldAccess v |
72+
v = e and
73+
v.getField() = annotated and
74+
// depending on the visiblity of the field, using the annotation to abuse the visibility may/may not be occurring
75+
(
76+
// if its package protected report when its used outside its class bc it should have been private (class only permitted)
77+
v.getField().isPackageProtected() and
78+
not isWithinType(v.getEnclosingCallable(), v.getField().getDeclaringType())
79+
or
80+
// if public or protected report when its used outside its package because package protected should have been enough (package only permitted)
81+
(v.getField().isPublic() or v.getField().isProtected()) and
82+
not isWithinPackage(v.getEnclosingCallable(), v.getField().getDeclaringType())
83+
)
84+
)
85+
or
86+
// class instantiation
87+
exists(ClassInstanceExpr c |
88+
c = e and
89+
c.getConstructedType() = annotated and
90+
// depending on the visiblity of the class, using the annotation to abuse the visibility may/may not be occurring
91+
// if public report when its used outside its package because package protected should have been enough (package only permitted)
92+
(
93+
c.getConstructedType().isPublic() and
94+
not isWithinPackage(c.getEnclosingCallable(), c.getConstructedType())
95+
or
96+
// if its package protected report when its used outside its outer class bc it should have been private (outer class only permitted)
97+
c.getConstructedType().hasNoModifier() and
98+
// and the class is an innerclass, because otherwise recommending a lower accessibility makes no sense (only inner classes can be private)
99+
exists(enclosingInstanceType(c.getConstructedType())) and
100+
not isWithinDirectOuterClassOrSiblingInner(c.getEnclosingCallable(), c.getConstructedType())
101+
)
102+
)
103+
or
104+
// method access
105+
exists(MethodCall c |
106+
c = e and
107+
c.getMethod() = annotated and
108+
// depending on the visiblity of the method, using the annotation to abuse the visibility may/may not be occurring
109+
(
110+
// if its package protected report when its used outside its class bc it should have been private (class only permitted)
111+
c.getMethod().isPackageProtected() and
112+
not isWithinType(c.getEnclosingCallable(), c.getMethod().getDeclaringType())
113+
or
114+
// if public or protected report when its used outside its package because package protected should have been enough (package only permitted)
115+
(c.getMethod().isPublic() or c.getMethod().isProtected()) and
116+
not isWithinPackage(c.getEnclosingCallable(), c.getMethod().getDeclaringType())
117+
)
118+
)
119+
) and
120+
// not in a test where use is appropriate
121+
not e.getEnclosingCallable() instanceof LikelyTestMethod and
122+
// also omit our own ql unit test where it is acceptable
123+
not e.getEnclosingCallable()
124+
.getFile()
125+
.getAbsolutePath()
126+
.matches("%java/ql/test/query-tests/%Test.java")
127+
select e, "Access of $@ annotated with VisibleForTesting found in production code.", annotated,
128+
"element"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
| packageone/SourcePackage.java:8:21:8:32 | Annotated.m1 | Access of $@ annotated with VisibleForTesting found in production code. | packagetwo/Annotated.java:9:29:9:30 | m1 | element |
2+
| packagetwo/Source.java:7:17:7:29 | f(...) | Access of $@ annotated with VisibleForTesting found in production code. | packagetwo/Annotated.java:12:16:12:16 | f | element |
3+
| packagetwo/Source.java:8:20:8:30 | Annotated.m | Access of $@ annotated with VisibleForTesting found in production code. | packagetwo/Annotated.java:7:19:7:19 | m | element |
4+
| packagetwo/Source.java:9:28:9:47 | new AnnotatedClass(...) | Access of $@ annotated with VisibleForTesting found in production code. | packageone/AnnotatedClass.java:4:14:4:27 | AnnotatedClass | element |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Violations of Best Practice/Implementation Hiding/VisibleForTestingAbuse.ql
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package packageone;
2+
3+
@VisibleForTesting
4+
public class AnnotatedClass {
5+
public AnnotatedClass() {}
6+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package packageone;
2+
3+
import packagetwo.Annotated;
4+
5+
public class SourcePackage extends Annotated {
6+
void f() {
7+
AnnotatedClass a = new AnnotatedClass(); // COMPLIANT - same package
8+
String s1 = Annotated.m1; // NON_COMPLIANT
9+
}
10+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package packageone;
2+
3+
public @interface VisibleForTesting {
4+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package packagetwo;
2+
3+
import packageone.*;
4+
5+
public class Annotated {
6+
@VisibleForTesting
7+
static String m;
8+
@VisibleForTesting
9+
static protected String m1;
10+
11+
@VisibleForTesting
12+
static int f() {
13+
return 1;
14+
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package packagetwo;
2+
3+
import packageone.*;
4+
5+
public class Source {
6+
void f() {
7+
int i = Annotated.f(); // NON_COMPLIANT
8+
String s = Annotated.m; // NON_COMPLIANT
9+
AnnotatedClass a = new AnnotatedClass(); // NON_COMPLIANT
10+
String s1 = Annotated.m1; // COMPLIANT - same package
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package packagetwo;
2+
3+
import packageone.*;
4+
5+
public class Test {
6+
void f() {
7+
int i = Annotated.f(); // COMPLIANT
8+
String s = Annotated.m; // COMPLIANT
9+
AnnotatedClass a = new AnnotatedClass(); // COMPLIANT
10+
String s1 = Annotated.m1; // COMPLIANT
11+
}
12+
}

0 commit comments

Comments
 (0)