Skip to content

Commit 2be3abd

Browse files
cpovirkError Prone Team
authored andcommitted
Fix ProtectedMembersInFinalClass to avoid suggesting removing protected from canonical constructors of protected record classes.
Along the way, introduce `ASTHelpers.isCanonicalRecordConstructor` to share some logic. Compare 1bb55ab. PiperOrigin-RevId: 942157685
1 parent 1bb55ab commit 2be3abd

5 files changed

Lines changed: 105 additions & 22 deletions

File tree

check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import static com.google.common.collect.ImmutableSet.toImmutableSet;
2323
import static com.google.common.collect.MoreCollectors.onlyElement;
2424
import static com.google.common.collect.Streams.stream;
25-
import static com.google.common.collect.Streams.zip;
2625
import static com.google.errorprone.VisitorState.memoize;
2726
import static com.google.errorprone.matchers.JUnitMatchers.JUNIT4_RUN_WITH_ANNOTATION;
2827
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
@@ -96,6 +95,7 @@
9695
import com.sun.tools.javac.code.Symbol.ClassSymbol;
9796
import com.sun.tools.javac.code.Symbol.MethodSymbol;
9897
import com.sun.tools.javac.code.Symbol.PackageSymbol;
98+
import com.sun.tools.javac.code.Symbol.RecordComponent;
9999
import com.sun.tools.javac.code.Symbol.TypeSymbol;
100100
import com.sun.tools.javac.code.Symbol.VarSymbol;
101101
import com.sun.tools.javac.code.Symtab;
@@ -829,21 +829,41 @@ public static boolean isRecord(Symbol symbol) {
829829

830830
/** Finds the canonical constructor on a record. */
831831
public static MethodSymbol canonicalConstructor(ClassSymbol record, VisitorState state) {
832-
var fieldTypes =
833-
record.getRecordComponents().stream().map(rc -> rc.type).collect(toImmutableList());
832+
var recordComponents = record.getRecordComponents();
834833
return stream(record.members().getSymbols(s -> s.isConstructor()))
835834
.map(c -> (MethodSymbol) c)
836-
.filter(
837-
c ->
838-
c.getParameters().size() == fieldTypes.size()
839-
&& zip(
840-
c.getParameters().stream(),
841-
fieldTypes.stream(),
842-
(a, b) -> isSameType(a.type, b, state))
843-
.allMatch(x -> x))
835+
.filter(c -> parametersMatchRecordComponents(c, recordComponents, state))
844836
.collect(onlyElement());
845837
}
846838

839+
/** Returns whether the given method is a record's canonical constructor. */
840+
public static boolean isCanonicalRecordConstructor(MethodSymbol symbol, VisitorState state) {
841+
if (!symbol.isConstructor()) {
842+
return false;
843+
}
844+
ClassSymbol enclosingClass = symbol.enclClass();
845+
if (enclosingClass == null || enclosingClass.getKind() != ElementKind.RECORD) {
846+
return false;
847+
}
848+
return parametersMatchRecordComponents(symbol, enclosingClass.getRecordComponents(), state);
849+
}
850+
851+
private static boolean parametersMatchRecordComponents(
852+
MethodSymbol constructor,
853+
List<? extends RecordComponent> recordComponents,
854+
VisitorState state) {
855+
if (constructor.getParameters().size() != recordComponents.size()) {
856+
return false;
857+
}
858+
for (int i = 0; i < recordComponents.size(); i++) {
859+
if (!isSameType(
860+
constructor.getParameters().get(i).type, recordComponents.get(i).type, state)) {
861+
return false;
862+
}
863+
}
864+
return true;
865+
}
866+
847867
/**
848868
* Determines whether a symbol has an annotation of the given type. This includes annotations
849869
* inherited from superclasses due to {@code @Inherited}.

check_api/src/test/java/com/google/errorprone/util/ASTHelpersTest.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static com.google.errorprone.util.ASTHelpers.getStartPosition;
2727
import static com.google.errorprone.util.ASTHelpers.getSymbol;
2828
import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
29+
import static com.google.errorprone.util.ASTHelpers.isCanonicalRecordConstructor;
2930
import static java.lang.annotation.ElementType.FIELD;
3031
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
3132
import static java.lang.annotation.ElementType.METHOD;
@@ -81,6 +82,7 @@
8182
import com.sun.source.util.TreePathScanner;
8283
import com.sun.tools.javac.code.Symbol;
8384
import com.sun.tools.javac.code.Symbol.ClassSymbol;
85+
import com.sun.tools.javac.code.Symbol.MethodSymbol;
8486
import com.sun.tools.javac.code.Symbol.PackageSymbol;
8587
import com.sun.tools.javac.code.Type;
8688
import com.sun.tools.javac.code.Type.TypeVar;
@@ -2065,9 +2067,12 @@ public static final class CanonicalConstructorFinder extends BugChecker
20652067
implements MethodTreeMatcher {
20662068
@Override
20672069
public Description matchMethod(MethodTree tree, VisitorState state) {
2068-
return canonicalConstructor((ClassSymbol) getSymbol(tree).owner, state) == getSymbol(tree)
2069-
? describeMatch(tree)
2070-
: Description.NO_MATCH;
2070+
MethodSymbol symbol = getSymbol(tree);
2071+
boolean isCanonical = isCanonicalRecordConstructor(symbol, state);
2072+
boolean isReturnedByCanonicalConstructorMethod =
2073+
symbol.equals(canonicalConstructor((ClassSymbol) symbol.owner, state));
2074+
assertThat(isCanonical).isEqualTo(isReturnedByCanonicalConstructorMethod);
2075+
return isCanonical ? describeMatch(tree) : Description.NO_MATCH;
20712076
}
20722077
}
20732078

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@
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.canonicalConstructor;
2322
import static com.google.errorprone.util.ASTHelpers.enclosingClass;
2423
import static com.google.errorprone.util.ASTHelpers.getSymbol;
2524
import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
25+
import static com.google.errorprone.util.ASTHelpers.isCanonicalRecordConstructor;
2626
import static com.google.errorprone.util.ASTHelpers.isEffectivelyPrivate;
27-
import static com.google.errorprone.util.ASTHelpers.isRecord;
2827
import static com.google.errorprone.util.ASTHelpers.streamSuperMethods;
2928
import static javax.lang.model.element.Modifier.PROTECTED;
3029
import static javax.lang.model.element.Modifier.PUBLIC;
@@ -108,12 +107,7 @@ private void match(
108107
}
109108
if (methodSymbol.isConstructor()) {
110109
ClassSymbol enclosingClass = enclosingClass(methodSymbol);
111-
/*
112-
* TODO(cpovirk): Introduce an ASTHelpers.isCanonicalConstructor to avoid scanning all
113-
* members again in canonicalConstructor?
114-
*/
115-
if (isRecord(enclosingClass)
116-
&& methodSymbol.equals(canonicalConstructor(enclosingClass, state))) {
110+
if (isCanonicalRecordConstructor(methodSymbol, state)) {
117111
if (enclosingClass.getModifiers().contains(PUBLIC)
118112
|| enclosingClass.getModifiers().contains(PROTECTED)) {
119113
// Canonical constructors are required to be at least as visible as the record itself.

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
2121
import static com.google.errorprone.matchers.Description.NO_MATCH;
2222
import static com.google.errorprone.matchers.Matchers.hasModifier;
23+
import static com.google.errorprone.util.ASTHelpers.getSymbol;
24+
import static com.google.errorprone.util.ASTHelpers.isCanonicalRecordConstructor;
2325
import static java.util.stream.Collectors.joining;
2426
import static javax.lang.model.element.Modifier.FINAL;
2527
import static javax.lang.model.element.Modifier.PROTECTED;
@@ -38,6 +40,7 @@
3840
import com.sun.source.tree.Tree;
3941
import com.sun.source.tree.VariableTree;
4042
import com.sun.tools.javac.code.Symbol;
43+
import com.sun.tools.javac.code.Symbol.MethodSymbol;
4144

4245
/**
4346
* Flags protected members in final classes.
@@ -57,6 +60,15 @@ private static boolean methodHasNoParentMethod(MethodTree methodTree, VisitorSta
5760
.isEmpty();
5861
}
5962

63+
private static boolean isConstructorOfProtectedRecord(Tree tree, VisitorState state) {
64+
if (!(tree instanceof MethodTree methodTree)) {
65+
return false;
66+
}
67+
MethodSymbol symbol = getSymbol(methodTree);
68+
return symbol.enclClass().getModifiers().contains(PROTECTED)
69+
&& isCanonicalRecordConstructor(symbol, state);
70+
}
71+
6072
@Override
6173
public Description matchClass(ClassTree tree, VisitorState state) {
6274
if (!HAS_FINAL.matches(tree, state)) {
@@ -71,6 +83,7 @@ public Description matchClass(ClassTree tree, VisitorState state) {
7183
m ->
7284
!(m instanceof MethodTree methodTree)
7385
|| methodHasNoParentMethod(methodTree, state))
86+
.filter(m -> !isConstructorOfProtectedRecord(m, state))
7487
.filter(m -> !isSuppressed(m, state))
7588
.collect(toImmutableList());
7689
if (relevantMembers.isEmpty()) {

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,55 @@ protected Test() {}
151151
""")
152152
.doTest();
153153
}
154+
155+
@Test
156+
public void protectedRecordClassCanonicalConstructor() {
157+
compilationHelper
158+
.addSourceLines(
159+
"in/Outer.java",
160+
"""
161+
public class Outer {
162+
protected record NestedRecord(int x) {
163+
protected NestedRecord {}
164+
}
165+
}
166+
""")
167+
.doTest();
168+
}
169+
170+
@Test
171+
public void packagePrivateRecordClassCanonicalConstructor() {
172+
compilationHelper
173+
.addSourceLines(
174+
"in/Outer.java",
175+
"""
176+
public class Outer {
177+
record NestedRecord(int x) {
178+
// BUG: Diagnostic contains: NestedRecord
179+
protected NestedRecord {}
180+
}
181+
}
182+
""")
183+
.doTest();
184+
}
185+
186+
@Test
187+
public void protectedRecordClassAuxiliaryConstructor() {
188+
compilationHelper
189+
.addSourceLines(
190+
"in/Outer.java",
191+
"""
192+
public class Outer {
193+
protected record NestedRecord(int x) {
194+
protected NestedRecord {}
195+
196+
// BUG: Diagnostic contains: NestedRecord
197+
protected NestedRecord(int x, int y) {
198+
this(x + y);
199+
}
200+
}
201+
}
202+
""")
203+
.doTest();
204+
}
154205
}

0 commit comments

Comments
 (0)