Skip to content

Commit aa6efec

Browse files
cpovirkError Prone Team
authored andcommitted
EffectivelyPrivate: Exclude anything declared within an effectively private class that has a visible subclass in the same file.
See discussion in #5182. PiperOrigin-RevId: 941277363
1 parent 7e25469 commit aa6efec

2 files changed

Lines changed: 121 additions & 34 deletions

File tree

core/src/main/java/com/google/errorprone/bugpatterns/EffectivelyPrivate.java

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
2020
import static com.google.errorprone.fixes.SuggestedFixes.removeModifiers;
2121
import static com.google.errorprone.matchers.Description.NO_MATCH;
22+
import static com.google.errorprone.util.ASTHelpers.enclosingClass;
2223
import static com.google.errorprone.util.ASTHelpers.getSymbol;
2324
import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
2425
import static com.google.errorprone.util.ASTHelpers.isEffectivelyPrivate;
@@ -27,31 +28,29 @@
2728
import com.google.common.collect.ImmutableSet;
2829
import com.google.errorprone.BugPattern;
2930
import com.google.errorprone.VisitorState;
30-
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
31-
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
32-
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
33-
import com.google.errorprone.fixes.SuggestedFix;
31+
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher;
3432
import com.google.errorprone.matchers.Description;
3533
import com.sun.source.tree.ClassTree;
34+
import com.sun.source.tree.CompilationUnitTree;
3635
import com.sun.source.tree.MethodTree;
3736
import com.sun.source.tree.ModifiersTree;
3837
import com.sun.source.tree.Tree;
3938
import com.sun.source.tree.VariableTree;
39+
import com.sun.source.util.TreePathScanner;
4040
import com.sun.tools.javac.code.Symbol;
41+
import com.sun.tools.javac.code.Symbol.ClassSymbol;
4142
import com.sun.tools.javac.code.Symbol.MethodSymbol;
4243
import com.sun.tools.javac.code.Symbol.VarSymbol;
44+
import com.sun.tools.javac.code.Type;
4345
import java.util.Collections;
44-
import java.util.Optional;
4546
import javax.inject.Inject;
4647
import javax.lang.model.element.Modifier;
4748

4849
/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
4950
@BugPattern(
5051
summary = "This declaration has public or protected modifiers, but is effectively private.",
5152
severity = WARNING)
52-
public final class EffectivelyPrivate extends BugChecker
53-
implements MethodTreeMatcher, VariableTreeMatcher, ClassTreeMatcher {
54-
53+
public final class EffectivelyPrivate extends BugChecker implements CompilationUnitTreeMatcher {
5554
private final WellKnownKeep wellKnownKeep;
5655

5756
@Inject
@@ -60,55 +59,89 @@ public final class EffectivelyPrivate extends BugChecker
6059
}
6160

6261
@Override
63-
public Description matchVariable(VariableTree tree, VisitorState state) {
64-
VarSymbol sym = getSymbol(tree);
65-
if (!sym.getKind().isField()) {
66-
return NO_MATCH;
67-
}
68-
return match(tree, tree.getModifiers(), state);
69-
}
62+
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) {
63+
ImmutableSet<ClassSymbol> hasVisibleSubclass = findClassesWithVisibleSubclasses(tree, state);
64+
new SuppressibleTreePathScanner<Void, Void>(state) {
65+
@Override
66+
public Void visitVariable(VariableTree tree, Void unused) {
67+
VarSymbol sym = getSymbol(tree);
68+
if (sym.getKind().isField()) {
69+
match(tree, tree.getModifiers(), hasVisibleSubclass, state);
70+
}
71+
return super.visitVariable(tree, null);
72+
}
7073

71-
@Override
72-
public Description matchClass(ClassTree tree, VisitorState state) {
73-
return match(tree, tree.getModifiers(), state);
74-
}
74+
@Override
75+
public Void visitClass(ClassTree tree, Void unused) {
76+
match(tree, tree.getModifiers(), hasVisibleSubclass, state);
77+
return super.visitClass(tree, null);
78+
}
7579

76-
@Override
77-
public Description matchMethod(MethodTree tree, VisitorState state) {
78-
return match(tree, tree.getModifiers(), state);
80+
@Override
81+
public Void visitMethod(MethodTree tree, Void unused) {
82+
match(tree, tree.getModifiers(), hasVisibleSubclass, state);
83+
return super.visitMethod(tree, null);
84+
}
85+
}.scan(tree, null);
86+
return NO_MATCH;
7987
}
8088

81-
private Description match(Tree tree, ModifiersTree modifiers, VisitorState state) {
89+
private void match(
90+
Tree tree,
91+
ModifiersTree modifiers,
92+
ImmutableSet<ClassSymbol> hasVisibleSubclass,
93+
VisitorState state) {
8294
Symbol sym = getSymbol(tree);
8395
if (!isEffectivelyPrivate(sym)) {
84-
return NO_MATCH;
96+
return;
8597
}
8698
if (wellKnownKeep.shouldKeep(tree)) {
87-
return NO_MATCH;
99+
return;
88100
}
89101
if (sym instanceof MethodSymbol methodSymbol) {
90102
if (hasAnnotation(methodSymbol, "java.lang.Override", state)) {
91-
return NO_MATCH;
103+
return;
92104
}
93105
// TODO: cushon - technically this should only match final classes, otherwise it could break
94106
// a subclass that relies on inheriting a method of a particular visibility to fulfil and
95107
// interface contract. Skip that for now, since many classes don't rely on that and also
96108
// aren't explicitly final.
97109
if (streamSuperMethods(methodSymbol, state.getTypes()).findAny().isPresent()) {
98-
return NO_MATCH;
110+
return;
99111
}
100112
}
101-
if (Collections.disjoint(modifiers.getFlags(), MODIFIER_TO_REMOVE)) {
102-
return NO_MATCH;
113+
var enclosingClass = enclosingClass(sym);
114+
if (hasVisibleSubclass.contains(enclosingClass)) {
115+
return;
103116
}
104-
Optional<SuggestedFix> fix = removeModifiers(modifiers, state, MODIFIER_TO_REMOVE);
105-
if (fix.isEmpty()) {
106-
// The fix may be empty for implicit modifiers, e.g. on enum constant fields
107-
return NO_MATCH;
117+
if (Collections.disjoint(modifiers.getFlags(), MODIFIER_TO_REMOVE)) {
118+
return;
108119
}
109-
return describeMatch(tree, fix.get());
120+
removeModifiers(modifiers, state, MODIFIER_TO_REMOVE)
121+
// The fix may be empty for implicit modifiers, e.g. on enum constant fields
122+
.ifPresent(fix -> state.reportMatch(describeMatch(tree, fix)));
110123
}
111124

112125
private static final ImmutableSet<Modifier> MODIFIER_TO_REMOVE =
113126
ImmutableSet.of(Modifier.PUBLIC, Modifier.PROTECTED);
127+
128+
private static ImmutableSet<ClassSymbol> findClassesWithVisibleSubclasses(
129+
CompilationUnitTree compilationUnit, VisitorState state) {
130+
ImmutableSet.Builder<ClassSymbol> hasVisibleSubclass = ImmutableSet.builder();
131+
new TreePathScanner<Void, Void>() {
132+
@Override
133+
public Void visitClass(ClassTree tree, Void unused) {
134+
ClassSymbol sym = getSymbol(tree);
135+
if (!isEffectivelyPrivate(sym)) {
136+
for (Type superType : state.getTypes().closure(sym.type)) {
137+
if (superType.tsym instanceof ClassSymbol classSymbol) {
138+
hasVisibleSubclass.add(classSymbol);
139+
}
140+
}
141+
}
142+
return super.visitClass(tree, null);
143+
}
144+
}.scan(compilationUnit, null);
145+
return hasVisibleSubclass.build();
146+
}
114147
}

core/src/test/java/com/google/errorprone/bugpatterns/EffectivelyPrivateTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,58 @@ public void run() {}
7373
""")
7474
.doTest();
7575
}
76+
77+
@Test
78+
public void negative_madeVisibleBySubclass() {
79+
testHelper
80+
.addSourceLines(
81+
"Test.java",
82+
"""
83+
class T {
84+
private class A {
85+
public void foo() {}
86+
}
87+
88+
public class B extends A {}
89+
}
90+
""")
91+
.doTest();
92+
}
93+
94+
@Test
95+
public void positive_subclassAlsoPrivate() {
96+
testHelper
97+
.addSourceLines(
98+
"Test.java",
99+
"""
100+
class T {
101+
private class A {
102+
// BUG: Diagnostic contains:
103+
public void foo() {}
104+
}
105+
106+
private class B extends A {}
107+
}
108+
""")
109+
.doTest();
110+
}
111+
112+
@Test
113+
public void negative_chainOfPrivateClasses() {
114+
testHelper
115+
.addSourceLines(
116+
"Test.java",
117+
"""
118+
class T {
119+
private class A {
120+
public void foo() {}
121+
}
122+
123+
private class B extends A {}
124+
125+
public class C extends B {}
126+
}
127+
""")
128+
.doTest();
129+
}
76130
}

0 commit comments

Comments
 (0)