From 4487ec00f7338d52ce9001a2e6b89aa1dc79cd2b Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Wed, 3 May 2023 12:46:40 +0200 Subject: [PATCH 01/51] Naive impl --- .../bugpatterns/MemberOrdering.java | 146 +++++++++++ .../bugpatterns/MemberOrderingTest.java | 232 ++++++++++++++++++ 2 files changed, 378 insertions(+) create mode 100644 error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java create mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java new file mode 100644 index 0000000000..b0799fbe6a --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -0,0 +1,146 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.errorprone.BugPattern.LinkType.CUSTOM; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.BugPattern.StandardTags.STYLE; +import static com.google.errorprone.fixes.SuggestedFixes.addSuppressWarnings; +import static com.sun.source.tree.Tree.Kind.METHOD; +import static com.sun.source.tree.Tree.Kind.VARIABLE; +import static java.util.Comparator.comparing; +import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; + +import com.google.auto.service.AutoService; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.fixes.SuggestedFixes; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.Tree; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; +import java.util.Comparator; +import java.util.Set; +import javax.lang.model.element.Modifier; + +@AutoService(BugChecker.class) +@BugPattern( + summary = "Members should be ordered in a standard way.", + link = BUG_PATTERNS_BASE_URL + "MemberOrdering", + linkType = CUSTOM, + severity = WARNING, + tags = STYLE) +public class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { + private static final long serialVersionUID = 1L; + /** A comparator that sorts members, constructors and methods in a standard order. */ + private static final Comparator COMPARATOR = + comparing( + (Tree memberTree) -> { + switch (memberTree.getKind()) { + case VARIABLE: + return 0; + case METHOD: + return 1; + } + throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); + }) + .thenComparing( + (Tree memberTree) -> { + switch (memberTree.getKind()) { + case VARIABLE: + return isStatic((JCVariableDecl) memberTree) ? 0 : 1; + case METHOD: + return isConstructor((JCMethodDecl) memberTree) ? 0 : 1; + } + throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); + }); + // XXX: Evaluate alternative implementation. + /** A comparator that sorts members, constructors and methods in a standard order. */ + private static final Comparator SQUASHED_COMPARATOR = + Comparator.comparing( + (Tree memberTree) -> { + if (memberTree.getKind() == VARIABLE) { + if (isStatic((JCVariableDecl) memberTree)) { + // 1. static variables. + return 1; + } else { + // 2. non-static variables. + return 2; + } + } + if (memberTree.getKind() == METHOD) { + if (isConstructor((JCMethodDecl) memberTree)) { + // 3. constructors. + return 3; + } else { + // 4. methods. + return 4; + } + } + throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); + }); + + /** Instantiates a new {@link MemberOrdering} instance. */ + public MemberOrdering() { + super(); + } + + @Override + public Description matchClass(ClassTree tree, VisitorState state) { + var members = + tree.getMembers().stream().filter(MemberOrdering::isHandled).collect(toImmutableList()); + var sortedMembers = members.stream().sorted(COMPARATOR).collect(toImmutableList()); + + if (members.equals(sortedMembers)) { + return Description.NO_MATCH; + } + + return buildDescription(tree) + .addFix(addSuppressWarnings(state, canonicalName())) + .addFix(swapMembers(state, members, sortedMembers)) + .setMessage( + "Members, constructors and methods should follow standard ordering. " + + "The standard ordering is: static variables, non-static variables, " + + "constructors and methods.") + .build(); + } + + private static boolean isHandled(Tree tree) { + return tree instanceof JCVariableDecl + || (tree instanceof JCMethodDecl + && !ASTHelpers.isGeneratedConstructor((JCMethodDecl) tree)); + } + + private static SuggestedFix swapMembers( + VisitorState state, + ImmutableList members, + ImmutableList sortedMembers) { + var fix = SuggestedFix.builder(); + for (int i = 0; i < members.size(); i++) { + Tree original = members.get(i); + Tree correct = sortedMembers.get(i); + // XXX: Technically not necessary, but avoids redundant replacements. + if (!original.equals(correct)) { + fix.replace(original, state.getSourceForNode(correct)); + } + } + return fix.build(); + } + + private static boolean isStatic(JCVariableDecl memberTree) { + Set modifiers = memberTree.getModifiers().getFlags(); + return modifiers.contains(Modifier.STATIC); + } + + private static boolean isConstructor(JCMethodDecl methodDecl) { + // XXX: Using state.getName(...) would be better, but than we'd need to introduce `state` as a + // parameter and that would mean we have to instantiate a COMPARATOR for every matchClass(...) + // call, which seems excessive for a simple check like this. + // That may also violate some kind of contract about comparators being stateless. + return methodDecl.getName().toString().equals(""); + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java new file mode 100644 index 0000000000..354a5d33fb --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -0,0 +1,232 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.SECOND; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +final class MemberOrderingTest { + @Test + void identification() { + CompilationTestHelper.newInstance(MemberOrdering.class, getClass()) + .addSourceLines( + "A.java", + "", + "// BUG: Diagnostic contains:", + "class A {", + " char a = 'a';", + " private static String FOO = \"foo\";", + " static int ONE = 1;", + "", + " void m2() {}", + "", + " public A () {}", + "", + " private static String BAR = \"bar\";", + " char b = 'b';", + "", + " void m1() {", + " System.out.println(\"foo\");", + " }", + " static int TWO = 2;", + "", + " class Inner {}", + " static class StaticInner {}", + "}") + .addSourceLines( + "B.java", + "", + "class B {", + " private static String FOO = \"foo\";", + " static int ONE = 1;", + " private static String BAR = \"bar\";", + "", + " static int TWO = 2;", + "", + " char a = 'a';", + "", + " char b = 'b';", + " public B () {}", + "", + " void m1() {", + " System.out.println(\"foo\");", + " }", + " void m2() {}", + "", + " class Inner {}", + " static class StaticInner {}", + "}"); + } + + @Test + void replacementFirstSuggestedFix() { + BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + .addInputLines( + "A.java", + "", + "class A {", + " private static final int X = 1;", + " char a = 'a';", + " private static String FOO = \"foo\";", + " static int ONE = 1;", + "", + " void m2() {}", + "", + " public A () {}", + "", + " private static String BAR = \"bar\";", + " char b = 'b';", + "", + " void m1() {", + " System.out.println(\"foo\");", + " }", + " static int TWO = 2;", + "", + " class Inner {}", + "", + " static class StaticInner {}", + "}") + .addOutputLines( + "A.java", + "", + "@SuppressWarnings(\"MemberOrdering\")", + "class A {", + " private static final int X = 1;", + " char a = 'a';", + " private static String FOO = \"foo\";", + " static int ONE = 1;", + "", + " void m2() {}", + "", + " public A () {}", + "", + " private static String BAR = \"bar\";", + " char b = 'b';", + "", + " void m1() {", + " System.out.println(\"foo\");", + " }", + " static int TWO = 2;", + "", + " class Inner {}", + "", + " static class StaticInner {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSecondSuggestedFix() { + BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + .setFixChooser(SECOND) + .addInputLines( + "A.java", + "", + "class A {", + " private static final int X = 1;", + " char a = 'a';", + " private static String FOO = \"foo\";", + " static int ONE = 1;", + "", + " void m2() {}", + "", + " public A () {}", + "", + " private static String BAR = \"bar\";", + " char b = 'b';", + "", + " void m1() {", + " System.out.println(\"foo\");", + " }", + " static int TWO = 2;", + "", + " class Inner {}", + "", + " static class StaticInner {}", + "}") + .addOutputLines( + "A.java", + "", + "class A {", + " private static final int X = 1;", + " private static String FOO = \"foo\";", + " static int ONE = 1;", + " private static String BAR = \"bar\";", + "", + " static int TWO = 2;", + "", + " char a = 'a';", + "", + " char b = 'b';", + " public A () {}", + "", + " void m2() {}", + " void m1() {", + " System.out.println(\"foo\");", + " }", + "", + " class Inner {}", + "", + " static class StaticInner {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSecondSuggestedFixWithDefaultConstructor() { + BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + .setFixChooser(SECOND) + .addInputLines( + "A.java", + "", + "class A {", + " void m1 () {}", + " char c = 'c';", + " private static final String foo = \"foo\";", + " static int one = 1;", + "}") + .addOutputLines( + "A.java", + "", + "class A {", + " private static final String foo = \"foo\";", + " static int one = 1;", + " char c = 'c';", + " void m1 () {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSecondSuggestedFixWithComments() { + BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + .setFixChooser(SECOND) + .addInputLines( + "A.java", + "", + "class A {", + " // `m1()` comment.", + " void m1() {", + " System.out.println(\"foo\");", + " }", + "", + " /** Instantiates a new {@link A} instance. */", + " public A () {}", + "}") + .addOutputLines( + "A.java", + "", + "class A {", + " /** Instantiates a new {@link A} instance. */", + " public A () {}", + "", + " // `m1()` comment.", + " void m1() {", + " System.out.println(\"foo\");", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } +} From 07c19c8bd2b41464b18b802994f306ac64f6f29f Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Tue, 16 May 2023 23:01:53 +0200 Subject: [PATCH 02/51] Consider comments --- .../bugpatterns/MemberOrdering.java | 117 +++++++++++++++++- .../bugpatterns/MemberOrderingTest.java | 4 + 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index b0799fbe6a..41a79098cd 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -5,6 +5,7 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.BugPattern.StandardTags.STYLE; import static com.google.errorprone.fixes.SuggestedFixes.addSuppressWarnings; +import static com.google.errorprone.util.ASTHelpers.getStartPosition; import static com.sun.source.tree.Tree.Kind.METHOD; import static com.sun.source.tree.Tree.Kind.VARIABLE; import static java.util.Comparator.comparing; @@ -12,19 +13,27 @@ import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.fixes.SuggestedFix; -import com.google.errorprone.fixes.SuggestedFixes; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; +import com.google.errorprone.util.ErrorProneToken; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; +import com.sun.tools.javac.parser.Tokens; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCVariableDecl; +import com.sun.tools.javac.util.Position; import java.util.Comparator; +import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.lang.model.element.Modifier; @AutoService(BugChecker.class) @@ -101,7 +110,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { return buildDescription(tree) .addFix(addSuppressWarnings(state, canonicalName())) - .addFix(swapMembers(state, members, sortedMembers)) + .addFix(swapMembersIncludingComments(members, sortedMembers, tree, state)) .setMessage( "Members, constructors and methods should follow standard ordering. " + "The standard ordering is: static variables, non-static variables, " @@ -115,17 +124,23 @@ private static boolean isHandled(Tree tree) { && !ASTHelpers.isGeneratedConstructor((JCMethodDecl) tree)); } - private static SuggestedFix swapMembers( - VisitorState state, + private static SuggestedFix swapMembersIncludingComments( ImmutableList members, - ImmutableList sortedMembers) { + ImmutableList sortedMembers, + ClassTree classTree, + VisitorState state) { var fix = SuggestedFix.builder(); for (int i = 0; i < members.size(); i++) { Tree original = members.get(i); Tree correct = sortedMembers.get(i); // XXX: Technically not necessary, but avoids redundant replacements. if (!original.equals(correct)) { - fix.replace(original, state.getSourceForNode(correct)); + var replacement = + Streams.concat( + getComments(classTree, correct, state), + Stream.of(state.getSourceForNode(correct))) + .collect(Collectors.joining("\n")); + fix.merge(replaceIncludingComments(classTree, original, replacement, state)); } } return fix.build(); @@ -143,4 +158,94 @@ private static boolean isConstructor(JCMethodDecl methodDecl) { // That may also violate some kind of contract about comparators being stateless. return methodDecl.getName().toString().equals(""); } + + private static Stream getComments(ClassTree classTree, Tree member, VisitorState state) { + var previousMember = getPreviousMember(member, classTree).orElse(null); + int startTokenization; + if (previousMember != null) { + startTokenization = state.getEndPosition(previousMember); + } else if (state.getEndPosition(classTree.getModifiers()) == Position.NOPOS) { + startTokenization = getStartPosition(classTree); + } else { + startTokenization = state.getEndPosition(classTree.getModifiers()); + } + List tokens = + state.getOffsetTokens(startTokenization, state.getEndPosition(member)); + if (previousMember == null) { + // todo: check if this is redundant. + tokens = getTokensAfterOpeningBrace(tokens); + } + if (tokens.isEmpty()) { + return Stream.empty(); + } + return tokens.get(0).comments().stream().map(c -> c.getText()); + } + + private static List getTokensAfterOpeningBrace(List tokens) { + for (int i = 0; i < tokens.size() - 1; ++i) { + if (tokens.get(i).kind() == Tokens.TokenKind.LBRACE) { + return tokens.subList(i + 1, tokens.size()); + } + } + return ImmutableList.of(); + } + + public static SuggestedFix replaceIncludingComments( + ClassTree classTree, Tree member, String replacement, VisitorState state) { + Tree previousMember = getPreviousMember(member, classTree).orElse(null); + int startTokenization; + // from here copy of `SuggestedFixes#replaceIncludingComments`. + if (previousMember != null) { + startTokenization = state.getEndPosition(previousMember); + } else if (state.getEndPosition(classTree.getModifiers()) == Position.NOPOS) { + startTokenization = getStartPosition(classTree); + } else { + startTokenization = state.getEndPosition(classTree.getModifiers()); + } + List tokens = + state.getOffsetTokens(startTokenization, state.getEndPosition(member)); + if (previousMember == null) { + tokens = getTokensAfterOpeningBrace(tokens); + } + if (tokens.isEmpty()) { + return SuggestedFix.replace(member, replacement); + } + if (tokens.get(0).comments().isEmpty()) { + return SuggestedFix.replace(tokens.get(0).pos(), state.getEndPosition(member), replacement); + } + ImmutableList comments = + ImmutableList.sortedCopyOf( + Comparator.comparingInt(c -> c.getSourcePos(0)).reversed(), + tokens.get(0).comments()); + int startPos = getStartPosition(member); + // This can happen for desugared expressions like `int a, b;`. + if (startPos < startTokenization) { + return SuggestedFix.emptyFix(); + } + // Delete backwards for comments which are not separated from our target by a blank line. + CharSequence sourceCode = state.getSourceCode(); + for (Tokens.Comment comment : comments) { + int endOfCommentPos = comment.getSourcePos(comment.getText().length() - 1); + CharSequence stringBetweenComments = sourceCode.subSequence(endOfCommentPos, startPos); + if (stringBetweenComments.chars().filter(c -> c == '\n').count() > 1) { + break; + } + startPos = comment.getSourcePos(0); + } + return SuggestedFix.replace(startPos, state.getEndPosition(member), replacement); + } + + private static Optional getPreviousMember(Tree tree, ClassTree classTree) { + Tree previousMember = null; + for (Tree member : classTree.getMembers()) { + if (member instanceof MethodTree && ASTHelpers.isGeneratedConstructor((MethodTree) member)) { + continue; + } + if (member.equals(tree)) { + break; + } + previousMember = member; + } + return Optional.ofNullable(previousMember); + } } diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java index 354a5d33fb..3d666e8c1d 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -209,9 +209,11 @@ void replacementSecondSuggestedFixWithComments() { "class A {", " // `m1()` comment.", " void m1() {", + " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", " }", "", + " // foo", " /** Instantiates a new {@link A} instance. */", " public A () {}", "}") @@ -219,11 +221,13 @@ void replacementSecondSuggestedFixWithComments() { "A.java", "", "class A {", + " // foo", " /** Instantiates a new {@link A} instance. */", " public A () {}", "", " // `m1()` comment.", " void m1() {", + " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", " }", "}") From f962899b45e35852e690e57037add53bb50beefc Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Wed, 17 May 2023 10:11:38 +0200 Subject: [PATCH 03/51] Prepare the code for others to see - kinda cleanup `replaceIncludingComments` copy - mark unsure parts of the code - list remaining tasks --- .../bugpatterns/MemberOrdering.java | 126 +++++++++--------- .../bugpatterns/MemberOrderingTest.java | 4 + 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 41a79098cd..e718222774 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -6,20 +6,23 @@ import static com.google.errorprone.BugPattern.StandardTags.STYLE; import static com.google.errorprone.fixes.SuggestedFixes.addSuppressWarnings; import static com.google.errorprone.util.ASTHelpers.getStartPosition; +import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor; import static com.sun.source.tree.Tree.Kind.METHOD; import static com.sun.source.tree.Tree.Kind.VARIABLE; +import static com.sun.tools.javac.parser.Tokens.TokenKind.LBRACE; import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.joining; import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.Var; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; -import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.ErrorProneToken; import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; @@ -32,7 +35,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import javax.lang.model.element.Modifier; @@ -120,8 +122,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { private static boolean isHandled(Tree tree) { return tree instanceof JCVariableDecl - || (tree instanceof JCMethodDecl - && !ASTHelpers.isGeneratedConstructor((JCMethodDecl) tree)); + || (tree instanceof JCMethodDecl && !isGeneratedConstructor((JCMethodDecl) tree)); } private static SuggestedFix swapMembersIncludingComments( @@ -136,77 +137,42 @@ private static SuggestedFix swapMembersIncludingComments( // XXX: Technically not necessary, but avoids redundant replacements. if (!original.equals(correct)) { var replacement = - Streams.concat( - getComments(classTree, correct, state), + Stream.concat( + getComments(classTree, correct, state).map(Tokens.Comment::getText), Stream.of(state.getSourceForNode(correct))) - .collect(Collectors.joining("\n")); + .collect(joining("\n")); fix.merge(replaceIncludingComments(classTree, original, replacement, state)); } } return fix.build(); } + // xxx: From this point the code is just a fancy copy of `SuggestFixes.replaceIncludingComments` - + // if we cannot use existing solutions for this functionality, this one needs a big refactor. + + private static Stream getComments( + ClassTree classTree, Tree member, VisitorState state) { + return getTokensBeforeMember(classTree, member, state).findFirst().stream() + .map(ErrorProneToken::comments) + // xxx: Original impl sorts comments, but that seems unnecessary. + .flatMap(List::stream); + } + private static boolean isStatic(JCVariableDecl memberTree) { Set modifiers = memberTree.getModifiers().getFlags(); return modifiers.contains(Modifier.STATIC); } private static boolean isConstructor(JCMethodDecl methodDecl) { - // XXX: Using state.getName(...) would be better, but than we'd need to introduce `state` as a - // parameter and that would mean we have to instantiate a COMPARATOR for every matchClass(...) - // call, which seems excessive for a simple check like this. - // That may also violate some kind of contract about comparators being stateless. - return methodDecl.getName().toString().equals(""); - } - - private static Stream getComments(ClassTree classTree, Tree member, VisitorState state) { - var previousMember = getPreviousMember(member, classTree).orElse(null); - int startTokenization; - if (previousMember != null) { - startTokenization = state.getEndPosition(previousMember); - } else if (state.getEndPosition(classTree.getModifiers()) == Position.NOPOS) { - startTokenization = getStartPosition(classTree); - } else { - startTokenization = state.getEndPosition(classTree.getModifiers()); - } - List tokens = - state.getOffsetTokens(startTokenization, state.getEndPosition(member)); - if (previousMember == null) { - // todo: check if this is redundant. - tokens = getTokensAfterOpeningBrace(tokens); - } - if (tokens.isEmpty()) { - return Stream.empty(); - } - return tokens.get(0).comments().stream().map(c -> c.getText()); - } - - private static List getTokensAfterOpeningBrace(List tokens) { - for (int i = 0; i < tokens.size() - 1; ++i) { - if (tokens.get(i).kind() == Tokens.TokenKind.LBRACE) { - return tokens.subList(i + 1, tokens.size()); - } - } - return ImmutableList.of(); + return getSymbol(methodDecl).isConstructor(); } public static SuggestedFix replaceIncludingComments( ClassTree classTree, Tree member, String replacement, VisitorState state) { - Tree previousMember = getPreviousMember(member, classTree).orElse(null); - int startTokenization; - // from here copy of `SuggestedFixes#replaceIncludingComments`. - if (previousMember != null) { - startTokenization = state.getEndPosition(previousMember); - } else if (state.getEndPosition(classTree.getModifiers()) == Position.NOPOS) { - startTokenization = getStartPosition(classTree); - } else { - startTokenization = state.getEndPosition(classTree.getModifiers()); - } - List tokens = - state.getOffsetTokens(startTokenization, state.getEndPosition(member)); - if (previousMember == null) { - tokens = getTokensAfterOpeningBrace(tokens); - } + Optional previousMember = getPreviousMember(member, classTree); + ImmutableList tokens = + getTokensBeforeMember(classTree, member, state).collect(toImmutableList()); + if (tokens.isEmpty()) { return SuggestedFix.replace(member, replacement); } @@ -217,9 +183,9 @@ public static SuggestedFix replaceIncludingComments( ImmutableList.sortedCopyOf( Comparator.comparingInt(c -> c.getSourcePos(0)).reversed(), tokens.get(0).comments()); - int startPos = getStartPosition(member); + @Var int startPos = getStartPosition(member); // This can happen for desugared expressions like `int a, b;`. - if (startPos < startTokenization) { + if (startPos < getStartTokenization(classTree, state, previousMember)) { return SuggestedFix.emptyFix(); } // Delete backwards for comments which are not separated from our target by a blank line. @@ -236,9 +202,9 @@ public static SuggestedFix replaceIncludingComments( } private static Optional getPreviousMember(Tree tree, ClassTree classTree) { - Tree previousMember = null; + @Var Tree previousMember = null; for (Tree member : classTree.getMembers()) { - if (member instanceof MethodTree && ASTHelpers.isGeneratedConstructor((MethodTree) member)) { + if (member instanceof MethodTree && isGeneratedConstructor((MethodTree) member)) { continue; } if (member.equals(tree)) { @@ -248,4 +214,38 @@ private static Optional getPreviousMember(Tree tree, ClassTree classTree) } return Optional.ofNullable(previousMember); } + + private static Stream getTokensBeforeMember( + ClassTree classTree, Tree member, VisitorState state) { + Optional previousMember = getPreviousMember(member, classTree); + var startTokenization = getStartTokenization(classTree, state, previousMember); + + Stream tokens = + state.getOffsetTokens(startTokenization, state.getEndPosition(member)).stream(); + + if (previousMember.isEmpty()) { + return tokens.dropWhile(token -> token.kind() != LBRACE).skip(1); + } else { + return tokens; + } + } + + // xxx: rename / remove method - it gets the start position of tokens that *might* be related to + // member. + // - afaik it includes a Class declaration if the member is the first member in the + // class and does not have comments. + private static Integer getStartTokenization( + ClassTree classTree, VisitorState state, Optional previousMember) { + return previousMember + .map(state::getEndPosition) + .orElseGet( + () -> + // xxx: could return the position of the character next to the opening brace of the + // class - `... Clazz ... {` + // this could make this method more defined, worthy of existence, but it may also + // require additional parameters and changes in `replaceIncludingComments` method. + state.getEndPosition(classTree.getModifiers()) == Position.NOPOS + ? getStartPosition(classTree) + : state.getEndPosition(classTree.getModifiers())); + } } diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java index 3d666e8c1d..076c4adff7 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -233,4 +233,8 @@ void replacementSecondSuggestedFixWithComments() { "}") .doTest(TestMode.TEXT_MATCH); } + + // todo: Test if second replacement considers annotations. + // todo: Chose between with, handles, considers, respects and regards for + // replacementSecondSuggestedFixXxxSomething } From 4140da3b36009e70df908d6f9adfeb7401f63cfe Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Thu, 18 May 2023 08:32:27 +0200 Subject: [PATCH 04/51] Test if error message contains crucial information --- .../errorprone/bugpatterns/MemberOrderingTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java index 076c4adff7..9d7599406d 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -1,5 +1,7 @@ package tech.picnic.errorprone.bugpatterns; +import static com.google.common.base.Predicates.and; +import static com.google.common.base.Predicates.containsPattern; import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.SECOND; import com.google.errorprone.BugCheckerRefactoringTestHelper; @@ -11,10 +13,16 @@ final class MemberOrderingTest { @Test void identification() { CompilationTestHelper.newInstance(MemberOrdering.class, getClass()) + .expectErrorMessage( + "MemberOrdering", + and( + containsPattern("SuppressWarnings"), + containsPattern( + "Members, constructors and methods should follow standard ordering."))) .addSourceLines( "A.java", "", - "// BUG: Diagnostic contains:", + "// BUG: Diagnostic matches: MemberOrdering", "class A {", " char a = 'a';", " private static String FOO = \"foo\";", @@ -57,7 +65,8 @@ void identification() { "", " class Inner {}", " static class StaticInner {}", - "}"); + "}") + .doTest(); } @Test From 5b522ae0172a702fc7dfccd2a4a7bcb8b5c0d345 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Thu, 18 May 2023 09:24:26 +0200 Subject: [PATCH 05/51] Code clean-up, formatting, docs, TEMPORARY supress warnings --- .../bugpatterns/MemberOrdering.java | 26 ++++++++++------- .../bugpatterns/MemberOrderingTest.java | 29 +++++++++---------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index e718222774..5793589d32 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -38,14 +38,18 @@ import java.util.stream.Stream; import javax.lang.model.element.Modifier; +/** A {@link BugChecker} that flags classes with non-standard member ordering. */ @AutoService(BugChecker.class) @BugPattern( summary = "Members should be ordered in a standard way.", + explanation = + "Members should be ordered in a standard way, which is: " + + "static member variables, non-static member variables, constructors, methods.", link = BUG_PATTERNS_BASE_URL + "MemberOrdering", linkType = CUSTOM, severity = WARNING, tags = STYLE) -public class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { +public final class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { private static final long serialVersionUID = 1L; /** A comparator that sorts members, constructors and methods in a standard order. */ private static final Comparator COMPARATOR = @@ -56,8 +60,9 @@ public class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMa return 0; case METHOD: return 1; + default: + throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); } - throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); }) .thenComparing( (Tree memberTree) -> { @@ -66,13 +71,16 @@ public class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMa return isStatic((JCVariableDecl) memberTree) ? 0 : 1; case METHOD: return isConstructor((JCMethodDecl) memberTree) ? 0 : 1; + default: + throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); } - throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); }); - // XXX: Evaluate alternative implementation. + + // todo: Evaluate alternative implementation. /** A comparator that sorts members, constructors and methods in a standard order. */ + @SuppressWarnings("unused") private static final Comparator SQUASHED_COMPARATOR = - Comparator.comparing( + comparing( (Tree memberTree) -> { if (memberTree.getKind() == VARIABLE) { if (isStatic((JCVariableDecl) memberTree)) { @@ -96,9 +104,7 @@ public class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMa }); /** Instantiates a new {@link MemberOrdering} instance. */ - public MemberOrdering() { - super(); - } + public MemberOrdering() {} @Override public Description matchClass(ClassTree tree, VisitorState state) { @@ -134,7 +140,7 @@ private static SuggestedFix swapMembersIncludingComments( for (int i = 0; i < members.size(); i++) { Tree original = members.get(i); Tree correct = sortedMembers.get(i); - // XXX: Technically not necessary, but avoids redundant replacements. + // xxx: Technically not necessary, but avoids redundant replacements. if (!original.equals(correct)) { var replacement = Stream.concat( @@ -167,7 +173,7 @@ private static boolean isConstructor(JCMethodDecl methodDecl) { return getSymbol(methodDecl).isConstructor(); } - public static SuggestedFix replaceIncludingComments( + private static SuggestedFix replaceIncludingComments( ClassTree classTree, Tree member, String replacement, VisitorState state) { Optional previousMember = getPreviousMember(member, classTree); ImmutableList tokens = diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java index 9d7599406d..a45e3ec74e 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -21,7 +21,6 @@ void identification() { "Members, constructors and methods should follow standard ordering."))) .addSourceLines( "A.java", - "", "// BUG: Diagnostic matches: MemberOrdering", "class A {", " char a = 'a';", @@ -30,7 +29,7 @@ void identification() { "", " void m2() {}", "", - " public A () {}", + " public A() {}", "", " private static String BAR = \"bar\";", " char b = 'b';", @@ -38,14 +37,15 @@ void identification() { " void m1() {", " System.out.println(\"foo\");", " }", + "", " static int TWO = 2;", "", " class Inner {}", + "", " static class StaticInner {}", "}") .addSourceLines( "B.java", - "", "class B {", " private static String FOO = \"foo\";", " static int ONE = 1;", @@ -56,14 +56,17 @@ void identification() { " char a = 'a';", "", " char b = 'b';", - " public B () {}", + "", + " public B() {}", "", " void m1() {", " System.out.println(\"foo\");", " }", + "", " void m2() {}", "", " class Inner {}", + "", " static class StaticInner {}", "}") .doTest(); @@ -74,7 +77,6 @@ void replacementFirstSuggestedFix() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) .addInputLines( "A.java", - "", "class A {", " private static final int X = 1;", " char a = 'a';", @@ -83,7 +85,7 @@ void replacementFirstSuggestedFix() { "", " void m2() {}", "", - " public A () {}", + " public A() {}", "", " private static String BAR = \"bar\";", " char b = 'b';", @@ -91,6 +93,7 @@ void replacementFirstSuggestedFix() { " void m1() {", " System.out.println(\"foo\");", " }", + "", " static int TWO = 2;", "", " class Inner {}", @@ -99,7 +102,6 @@ void replacementFirstSuggestedFix() { "}") .addOutputLines( "A.java", - "", "@SuppressWarnings(\"MemberOrdering\")", "class A {", " private static final int X = 1;", @@ -109,7 +111,7 @@ void replacementFirstSuggestedFix() { "", " void m2() {}", "", - " public A () {}", + " public A() {}", "", " private static String BAR = \"bar\";", " char b = 'b';", @@ -117,6 +119,7 @@ void replacementFirstSuggestedFix() { " void m1() {", " System.out.println(\"foo\");", " }", + "", " static int TWO = 2;", "", " class Inner {}", @@ -126,6 +129,7 @@ void replacementFirstSuggestedFix() { .doTest(TestMode.TEXT_MATCH); } + @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementSecondSuggestedFix() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) @@ -152,12 +156,10 @@ void replacementSecondSuggestedFix() { " static int TWO = 2;", "", " class Inner {}", - "", " static class StaticInner {}", "}") .addOutputLines( "A.java", - "", "class A {", " private static final int X = 1;", " private static String FOO = \"foo\";", @@ -177,12 +179,12 @@ void replacementSecondSuggestedFix() { " }", "", " class Inner {}", - "", " static class StaticInner {}", "}") .doTest(TestMode.TEXT_MATCH); } + @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementSecondSuggestedFixWithDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) @@ -208,32 +210,29 @@ void replacementSecondSuggestedFixWithDefaultConstructor() { .doTest(TestMode.TEXT_MATCH); } + @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementSecondSuggestedFixWithComments() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) .setFixChooser(SECOND) .addInputLines( "A.java", - "", "class A {", " // `m1()` comment.", " void m1() {", " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", " }", - "", " // foo", " /** Instantiates a new {@link A} instance. */", " public A () {}", "}") .addOutputLines( "A.java", - "", "class A {", " // foo", " /** Instantiates a new {@link A} instance. */", " public A () {}", - "", " // `m1()` comment.", " void m1() {", " // Print line 'foo' to stdout.", From d887fdcd7f661cb998fb683d8bd6836e8333dfc3 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Thu, 18 May 2023 19:01:10 +0200 Subject: [PATCH 06/51] Cannot verify whitespace related requirements as `TestMode.TEXT_MATCH` formats the sources --- .../bugpatterns/MemberOrderingTest.java | 119 +++++++++++++++--- 1 file changed, 105 insertions(+), 14 deletions(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java index a45e3ec74e..6642fa35b2 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -129,14 +129,12 @@ void replacementFirstSuggestedFix() { .doTest(TestMode.TEXT_MATCH); } - @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementSecondSuggestedFix() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) .setFixChooser(SECOND) .addInputLines( "A.java", - "", "class A {", " private static final int X = 1;", " char a = 'a';", @@ -145,7 +143,7 @@ void replacementSecondSuggestedFix() { "", " void m2() {}", "", - " public A () {}", + " public A() {}", "", " private static String BAR = \"bar\";", " char b = 'b';", @@ -153,9 +151,11 @@ void replacementSecondSuggestedFix() { " void m1() {", " System.out.println(\"foo\");", " }", + "", " static int TWO = 2;", "", " class Inner {}", + "", " static class StaticInner {}", "}") .addOutputLines( @@ -171,48 +171,49 @@ void replacementSecondSuggestedFix() { " char a = 'a';", "", " char b = 'b';", - " public A () {}", + "", + " public A() {}", "", " void m2() {}", + "", " void m1() {", " System.out.println(\"foo\");", " }", "", " class Inner {}", + "", " static class StaticInner {}", "}") .doTest(TestMode.TEXT_MATCH); } - @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test - void replacementSecondSuggestedFixWithDefaultConstructor() { + void replacementSecondSuggestedFixConsidersDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) .setFixChooser(SECOND) .addInputLines( "A.java", - "", "class A {", - " void m1 () {}", + " void m1() {}", + "", " char c = 'c';", " private static final String foo = \"foo\";", " static int one = 1;", "}") .addOutputLines( "A.java", - "", "class A {", " private static final String foo = \"foo\";", " static int one = 1;", " char c = 'c';", - " void m1 () {}", + "", + " void m1() {}", "}") .doTest(TestMode.TEXT_MATCH); } - @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test - void replacementSecondSuggestedFixWithComments() { + void replacementSecondSuggestedFixConsidersComments() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) .setFixChooser(SECOND) .addInputLines( @@ -223,16 +224,18 @@ void replacementSecondSuggestedFixWithComments() { " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", " }", + "", " // foo", " /** Instantiates a new {@link A} instance. */", - " public A () {}", + " public A() {}", "}") .addOutputLines( "A.java", "class A {", " // foo", " /** Instantiates a new {@link A} instance. */", - " public A () {}", + " public A() {}", + "", " // `m1()` comment.", " void m1() {", " // Print line 'foo' to stdout.", @@ -242,6 +245,94 @@ void replacementSecondSuggestedFixWithComments() { .doTest(TestMode.TEXT_MATCH); } + @SuppressWarnings("ErrorProneTestHelperSourceFormat") + @Test + void replacementSecondSuggestedFixDoesNotModifyWhitespace() { + BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + .setFixChooser(SECOND) + .addInputLines( + "A.java", + "", + "", + "class A {", + "", + "", + " // `m1()` comment.", + " void m1() {", + " // Print line 'foo' to stdout.", + " System.out.println(\"foo\");", + " }", + " public A () { }", + "", + "", + "}") + .addOutputLines( + "A.java", + "", + "", + "class A {", + "", + "", + "", + " public A () { }", + " // `m1()` comment.", + " void m1() {", + " // Print line 'foo' to stdout.", + " System.out.println(\"foo\");", + " }", + "", + "", + "}") + .doTest(); + } + + @SuppressWarnings("ErrorProneTestHelperSourceFormat") + @Test + void xxx() { // todo: Actually test that the whitespace is preserved. + BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + .setFixChooser(SECOND) + .addInputLines( + "A.java", + "", + "", + "class A {", + "", + "", + " // `m1()` comment.", + " void m1() {", + " // Print line 'foo' to stdout.", + " System.out.println(\"foo\");", + " }", + " public A () { }", + "", + "", + "}") + .addOutputLines( + "A.java", + "", + "", + "class A {", + "", + " ", + " ", + " \t \t", + " ", + " ", + "", + " public A () { }", + " // `m1()` comment.", + " void m1", + " ()", + " {", + " // Print line 'foo' to stdout.", + " System.out.println(\"foo\");", + " }", + "", + "", + "}") + .doTest(TestMode.TEXT_MATCH); + } + // todo: Test if second replacement considers annotations. // todo: Chose between with, handles, considers, respects and regards for // replacementSecondSuggestedFixXxxSomething From 1330a2f2528fff00e287c1a94cb5a2b05a3a1eb3 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Thu, 18 May 2023 19:27:57 +0200 Subject: [PATCH 07/51] Apply my suggestions - phrasing: last , -> and - Reasonably concise `COMPARATOR` and its docs --- .../bugpatterns/MemberOrdering.java | 58 +++---------------- .../bugpatterns/MemberOrderingTest.java | 9 ++- 2 files changed, 15 insertions(+), 52 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 5793589d32..cb94929684 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -8,8 +8,6 @@ import static com.google.errorprone.util.ASTHelpers.getStartPosition; import static com.google.errorprone.util.ASTHelpers.getSymbol; import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor; -import static com.sun.source.tree.Tree.Kind.METHOD; -import static com.sun.source.tree.Tree.Kind.VARIABLE; import static com.sun.tools.javac.parser.Tokens.TokenKind.LBRACE; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.joining; @@ -44,63 +42,25 @@ summary = "Members should be ordered in a standard way.", explanation = "Members should be ordered in a standard way, which is: " - + "static member variables, non-static member variables, constructors, methods.", + + "static member variables, non-static member variables, constructors and methods.", link = BUG_PATTERNS_BASE_URL + "MemberOrdering", linkType = CUSTOM, severity = WARNING, tags = STYLE) public final class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { private static final long serialVersionUID = 1L; - /** A comparator that sorts members, constructors and methods in a standard order. */ + /** A comparator that sorts variable and method (incl. constructors) in a standard order. */ private static final Comparator COMPARATOR = - comparing( - (Tree memberTree) -> { - switch (memberTree.getKind()) { - case VARIABLE: - return 0; - case METHOD: - return 1; - default: - throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); - } - }) - .thenComparing( - (Tree memberTree) -> { - switch (memberTree.getKind()) { - case VARIABLE: - return isStatic((JCVariableDecl) memberTree) ? 0 : 1; - case METHOD: - return isConstructor((JCMethodDecl) memberTree) ? 0 : 1; - default: - throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); - } - }); - - // todo: Evaluate alternative implementation. - /** A comparator that sorts members, constructors and methods in a standard order. */ - @SuppressWarnings("unused") - private static final Comparator SQUASHED_COMPARATOR = comparing( (Tree memberTree) -> { - if (memberTree.getKind() == VARIABLE) { - if (isStatic((JCVariableDecl) memberTree)) { - // 1. static variables. - return 1; - } else { - // 2. non-static variables. - return 2; - } - } - if (memberTree.getKind() == METHOD) { - if (isConstructor((JCMethodDecl) memberTree)) { - // 3. constructors. - return 3; - } else { - // 4. methods. - return 4; - } + switch (memberTree.getKind()) { + case VARIABLE: + return isStatic((JCVariableDecl) memberTree) ? 1 : 2; + case METHOD: + return isConstructor((JCMethodDecl) memberTree) ? 3 : 4; + default: + throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); } - throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); }); /** Instantiates a new {@link MemberOrdering} instance. */ diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java index 6642fa35b2..84e8e1f142 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -197,14 +197,18 @@ void replacementSecondSuggestedFixConsidersDefaultConstructor() { " void m1() {}", "", " char c = 'c';", + "", " private static final String foo = \"foo\";", + "", " static int one = 1;", "}") .addOutputLines( "A.java", "class A {", " private static final String foo = \"foo\";", + "", " static int one = 1;", + "", " char c = 'c';", "", " void m1() {}", @@ -286,9 +290,10 @@ void replacementSecondSuggestedFixDoesNotModifyWhitespace() { .doTest(); } + // todo: Actually verify that whitespace is preserved. @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test - void xxx() { // todo: Actually test that the whitespace is preserved. + void xxx() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) .setFixChooser(SECOND) .addInputLines( @@ -334,6 +339,4 @@ void xxx() { // todo: Actually test that the whitespace is preserved. } // todo: Test if second replacement considers annotations. - // todo: Chose between with, handles, considers, respects and regards for - // replacementSecondSuggestedFixXxxSomething } From fb76a555e74234920df9b2206e3a81d7620cb7fa Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 25 May 2023 13:31:32 +0200 Subject: [PATCH 08/51] Suggestions --- .../bugpatterns/MemberOrdering.java | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index cb94929684..b51c64eff9 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -4,10 +4,6 @@ import static com.google.errorprone.BugPattern.LinkType.CUSTOM; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.BugPattern.StandardTags.STYLE; -import static com.google.errorprone.fixes.SuggestedFixes.addSuppressWarnings; -import static com.google.errorprone.util.ASTHelpers.getStartPosition; -import static com.google.errorprone.util.ASTHelpers.getSymbol; -import static com.google.errorprone.util.ASTHelpers.isGeneratedConstructor; import static com.sun.tools.javac.parser.Tokens.TokenKind.LBRACE; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.joining; @@ -20,14 +16,15 @@ import com.google.errorprone.annotations.Var; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.fixes.SuggestedFixes; import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.ErrorProneToken; import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; import com.sun.tools.javac.parser.Tokens; -import com.sun.tools.javac.tree.JCTree.JCMethodDecl; -import com.sun.tools.javac.tree.JCTree.JCVariableDecl; import com.sun.tools.javac.util.Position; import java.util.Comparator; import java.util.List; @@ -49,15 +46,16 @@ tags = STYLE) public final class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { private static final long serialVersionUID = 1L; + /** A comparator that sorts variable and method (incl. constructors) in a standard order. */ - private static final Comparator COMPARATOR = + private static final Comparator MEMBER_SORTING = comparing( (Tree memberTree) -> { switch (memberTree.getKind()) { case VARIABLE: - return isStatic((JCVariableDecl) memberTree) ? 1 : 2; + return isStatic((VariableTree) memberTree) ? 1 : 2; case METHOD: - return isConstructor((JCMethodDecl) memberTree) ? 3 : 4; + return isConstructor((MethodTree) memberTree) ? 3 : 4; default: throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); } @@ -68,16 +66,19 @@ public MemberOrdering() {} @Override public Description matchClass(ClassTree tree, VisitorState state) { - var members = - tree.getMembers().stream().filter(MemberOrdering::isHandled).collect(toImmutableList()); - var sortedMembers = members.stream().sorted(COMPARATOR).collect(toImmutableList()); + ImmutableList members = + tree.getMembers().stream() + .filter(MemberOrdering::shouldBeSorted) + .collect(toImmutableList()); + ImmutableList sortedMembers = + members.stream().sorted(MEMBER_SORTING).collect(toImmutableList()); if (members.equals(sortedMembers)) { return Description.NO_MATCH; } return buildDescription(tree) - .addFix(addSuppressWarnings(state, canonicalName())) + .addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName())) .addFix(swapMembersIncludingComments(members, sortedMembers, tree, state)) .setMessage( "Members, constructors and methods should follow standard ordering. " @@ -86,9 +87,9 @@ public Description matchClass(ClassTree tree, VisitorState state) { .build(); } - private static boolean isHandled(Tree tree) { - return tree instanceof JCVariableDecl - || (tree instanceof JCMethodDecl && !isGeneratedConstructor((JCMethodDecl) tree)); + private static boolean shouldBeSorted(Tree tree) { + return tree instanceof VariableTree + || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)); } private static SuggestedFix swapMembersIncludingComments( @@ -96,13 +97,13 @@ private static SuggestedFix swapMembersIncludingComments( ImmutableList sortedMembers, ClassTree classTree, VisitorState state) { - var fix = SuggestedFix.builder(); + SuggestedFix.Builder fix = SuggestedFix.builder(); for (int i = 0; i < members.size(); i++) { Tree original = members.get(i); Tree correct = sortedMembers.get(i); - // xxx: Technically not necessary, but avoids redundant replacements. + /* Technically not necessary, but avoids redundant replacements. */ if (!original.equals(correct)) { - var replacement = + String replacement = Stream.concat( getComments(classTree, correct, state).map(Tokens.Comment::getText), Stream.of(state.getSourceForNode(correct))) @@ -113,8 +114,14 @@ private static SuggestedFix swapMembersIncludingComments( return fix.build(); } - // xxx: From this point the code is just a fancy copy of `SuggestFixes.replaceIncludingComments` - - // if we cannot use existing solutions for this functionality, this one needs a big refactor. + private static boolean isStatic(VariableTree memberTree) { + Set modifiers = memberTree.getModifiers().getFlags(); + return modifiers.contains(Modifier.STATIC); + } + + private static boolean isConstructor(MethodTree methodDecl) { + return ASTHelpers.getSymbol(methodDecl).isConstructor(); + } private static Stream getComments( ClassTree classTree, Tree member, VisitorState state) { @@ -124,14 +131,8 @@ private static Stream getComments( .flatMap(List::stream); } - private static boolean isStatic(JCVariableDecl memberTree) { - Set modifiers = memberTree.getModifiers().getFlags(); - return modifiers.contains(Modifier.STATIC); - } - - private static boolean isConstructor(JCMethodDecl methodDecl) { - return getSymbol(methodDecl).isConstructor(); - } + // XXX: From this point the code is just a fancy copy of `SuggestFixes.replaceIncludingComments` - + // if we cannot use existing solutions for this functionality, this one needs a big refactor. private static SuggestedFix replaceIncludingComments( ClassTree classTree, Tree member, String replacement, VisitorState state) { @@ -149,7 +150,7 @@ private static SuggestedFix replaceIncludingComments( ImmutableList.sortedCopyOf( Comparator.comparingInt(c -> c.getSourcePos(0)).reversed(), tokens.get(0).comments()); - @Var int startPos = getStartPosition(member); + @Var int startPos = ASTHelpers.getStartPosition(member); // This can happen for desugared expressions like `int a, b;`. if (startPos < getStartTokenization(classTree, state, previousMember)) { return SuggestedFix.emptyFix(); @@ -170,7 +171,7 @@ private static SuggestedFix replaceIncludingComments( private static Optional getPreviousMember(Tree tree, ClassTree classTree) { @Var Tree previousMember = null; for (Tree member : classTree.getMembers()) { - if (member instanceof MethodTree && isGeneratedConstructor((MethodTree) member)) { + if (member instanceof MethodTree && ASTHelpers.isGeneratedConstructor((MethodTree) member)) { continue; } if (member.equals(tree)) { @@ -184,19 +185,18 @@ private static Optional getPreviousMember(Tree tree, ClassTree classTree) private static Stream getTokensBeforeMember( ClassTree classTree, Tree member, VisitorState state) { Optional previousMember = getPreviousMember(member, classTree); - var startTokenization = getStartTokenization(classTree, state, previousMember); + Integer startTokenization = getStartTokenization(classTree, state, previousMember); Stream tokens = state.getOffsetTokens(startTokenization, state.getEndPosition(member)).stream(); if (previousMember.isEmpty()) { return tokens.dropWhile(token -> token.kind() != LBRACE).skip(1); - } else { - return tokens; } + return tokens; } - // xxx: rename / remove method - it gets the start position of tokens that *might* be related to + // XXX: rename / remove method - it gets the start position of tokens that *might* be related to // member. // - afaik it includes a Class declaration if the member is the first member in the // class and does not have comments. @@ -206,12 +206,12 @@ private static Integer getStartTokenization( .map(state::getEndPosition) .orElseGet( () -> - // xxx: could return the position of the character next to the opening brace of the + // XXX: Could return the position of the character next to the opening brace of the // class - `... Clazz ... {` // this could make this method more defined, worthy of existence, but it may also // require additional parameters and changes in `replaceIncludingComments` method. state.getEndPosition(classTree.getModifiers()) == Position.NOPOS - ? getStartPosition(classTree) + ? ASTHelpers.getStartPosition(classTree) : state.getEndPosition(classTree.getModifiers())); } } From 2e2973187f8462ed58bf99ad7673cab442f12f66 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 25 May 2023 13:34:21 +0200 Subject: [PATCH 09/51] Drop `replaceIncludingComments` --- .../bugpatterns/MemberOrdering.java | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index b51c64eff9..52f72682bc 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -108,7 +108,7 @@ private static SuggestedFix swapMembersIncludingComments( getComments(classTree, correct, state).map(Tokens.Comment::getText), Stream.of(state.getSourceForNode(correct))) .collect(joining("\n")); - fix.merge(replaceIncludingComments(classTree, original, replacement, state)); + fix.merge(SuggestedFixes.replaceIncludingComments(state.getPath(), replacement, state)); } } return fix.build(); @@ -131,43 +131,6 @@ private static Stream getComments( .flatMap(List::stream); } - // XXX: From this point the code is just a fancy copy of `SuggestFixes.replaceIncludingComments` - - // if we cannot use existing solutions for this functionality, this one needs a big refactor. - - private static SuggestedFix replaceIncludingComments( - ClassTree classTree, Tree member, String replacement, VisitorState state) { - Optional previousMember = getPreviousMember(member, classTree); - ImmutableList tokens = - getTokensBeforeMember(classTree, member, state).collect(toImmutableList()); - - if (tokens.isEmpty()) { - return SuggestedFix.replace(member, replacement); - } - if (tokens.get(0).comments().isEmpty()) { - return SuggestedFix.replace(tokens.get(0).pos(), state.getEndPosition(member), replacement); - } - ImmutableList comments = - ImmutableList.sortedCopyOf( - Comparator.comparingInt(c -> c.getSourcePos(0)).reversed(), - tokens.get(0).comments()); - @Var int startPos = ASTHelpers.getStartPosition(member); - // This can happen for desugared expressions like `int a, b;`. - if (startPos < getStartTokenization(classTree, state, previousMember)) { - return SuggestedFix.emptyFix(); - } - // Delete backwards for comments which are not separated from our target by a blank line. - CharSequence sourceCode = state.getSourceCode(); - for (Tokens.Comment comment : comments) { - int endOfCommentPos = comment.getSourcePos(comment.getText().length() - 1); - CharSequence stringBetweenComments = sourceCode.subSequence(endOfCommentPos, startPos); - if (stringBetweenComments.chars().filter(c -> c == '\n').count() > 1) { - break; - } - startPos = comment.getSourcePos(0); - } - return SuggestedFix.replace(startPos, state.getEndPosition(member), replacement); - } - private static Optional getPreviousMember(Tree tree, ClassTree classTree) { @Var Tree previousMember = null; for (Tree member : classTree.getMembers()) { From d10a9edaa26bd4a10c3974c5e50356225e98aa7e Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 25 May 2023 13:47:20 +0200 Subject: [PATCH 10/51] Revert "Drop `replaceIncludingComments`" This reverts commit 133c85da4f86a3d3965384acf4cdc24b3f50677b. --- .../bugpatterns/MemberOrdering.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 52f72682bc..b51c64eff9 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -108,7 +108,7 @@ private static SuggestedFix swapMembersIncludingComments( getComments(classTree, correct, state).map(Tokens.Comment::getText), Stream.of(state.getSourceForNode(correct))) .collect(joining("\n")); - fix.merge(SuggestedFixes.replaceIncludingComments(state.getPath(), replacement, state)); + fix.merge(replaceIncludingComments(classTree, original, replacement, state)); } } return fix.build(); @@ -131,6 +131,43 @@ private static Stream getComments( .flatMap(List::stream); } + // XXX: From this point the code is just a fancy copy of `SuggestFixes.replaceIncludingComments` - + // if we cannot use existing solutions for this functionality, this one needs a big refactor. + + private static SuggestedFix replaceIncludingComments( + ClassTree classTree, Tree member, String replacement, VisitorState state) { + Optional previousMember = getPreviousMember(member, classTree); + ImmutableList tokens = + getTokensBeforeMember(classTree, member, state).collect(toImmutableList()); + + if (tokens.isEmpty()) { + return SuggestedFix.replace(member, replacement); + } + if (tokens.get(0).comments().isEmpty()) { + return SuggestedFix.replace(tokens.get(0).pos(), state.getEndPosition(member), replacement); + } + ImmutableList comments = + ImmutableList.sortedCopyOf( + Comparator.comparingInt(c -> c.getSourcePos(0)).reversed(), + tokens.get(0).comments()); + @Var int startPos = ASTHelpers.getStartPosition(member); + // This can happen for desugared expressions like `int a, b;`. + if (startPos < getStartTokenization(classTree, state, previousMember)) { + return SuggestedFix.emptyFix(); + } + // Delete backwards for comments which are not separated from our target by a blank line. + CharSequence sourceCode = state.getSourceCode(); + for (Tokens.Comment comment : comments) { + int endOfCommentPos = comment.getSourcePos(comment.getText().length() - 1); + CharSequence stringBetweenComments = sourceCode.subSequence(endOfCommentPos, startPos); + if (stringBetweenComments.chars().filter(c -> c == '\n').count() > 1) { + break; + } + startPos = comment.getSourcePos(0); + } + return SuggestedFix.replace(startPos, state.getEndPosition(member), replacement); + } + private static Optional getPreviousMember(Tree tree, ClassTree classTree) { @Var Tree previousMember = null; for (Tree member : classTree.getMembers()) { From 7a5e504941e27c8ef7d247d3b133705c61e8a384 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 9 Jul 2023 11:59:04 +0200 Subject: [PATCH 11/51] Suggestions --- .../bugpatterns/MemberOrdering.java | 195 ++++++++---------- .../bugpatterns/MemberOrderingTest.java | 106 +++------- 2 files changed, 118 insertions(+), 183 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index b51c64eff9..370d714517 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -4,7 +4,6 @@ import static com.google.errorprone.BugPattern.LinkType.CUSTOM; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.BugPattern.StandardTags.STYLE; -import static com.sun.tools.javac.parser.Tokens.TokenKind.LBRACE; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.joining; import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; @@ -20,15 +19,16 @@ import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.ErrorProneToken; +import com.google.errorprone.util.ErrorProneTokens; import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; import com.sun.tools.javac.parser.Tokens; -import com.sun.tools.javac.util.Position; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.lang.model.element.Modifier; @@ -66,20 +66,21 @@ public MemberOrdering() {} @Override public Description matchClass(ClassTree tree, VisitorState state) { - ImmutableList members = - tree.getMembers().stream() - .filter(MemberOrdering::shouldBeSorted) + ImmutableList membersWithComments = + getMembersWithComments(tree, state).stream() + .filter(memberWithComments -> shouldBeSorted(memberWithComments.member())) .collect(toImmutableList()); - ImmutableList sortedMembers = - members.stream().sorted(MEMBER_SORTING).collect(toImmutableList()); - if (members.equals(sortedMembers)) { + ImmutableList sortedMembersWithComments = + ImmutableList.sortedCopyOf( + (a, b) -> MEMBER_SORTING.compare(a.member(), b.member()), membersWithComments); + + if (membersWithComments.equals(sortedMembersWithComments)) { return Description.NO_MATCH; } return buildDescription(tree) - .addFix(SuggestedFixes.addSuppressWarnings(state, canonicalName())) - .addFix(swapMembersIncludingComments(members, sortedMembers, tree, state)) + .addFix(swapMembersWithComments(membersWithComments, sortedMembersWithComments, state)) .setMessage( "Members, constructors and methods should follow standard ordering. " + "The standard ordering is: static variables, non-static variables, " @@ -87,131 +88,109 @@ public Description matchClass(ClassTree tree, VisitorState state) { .build(); } + private static boolean isStatic(VariableTree memberTree) { + Set modifiers = memberTree.getModifiers().getFlags(); + return modifiers.contains(Modifier.STATIC); + } + + private static boolean isConstructor(MethodTree methodDecl) { + return ASTHelpers.getSymbol(methodDecl).isConstructor(); + } + private static boolean shouldBeSorted(Tree tree) { return tree instanceof VariableTree || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)); } - private static SuggestedFix swapMembersIncludingComments( - ImmutableList members, - ImmutableList sortedMembers, - ClassTree classTree, + private static SuggestedFix swapMembersWithComments( + ImmutableList memberWithComments, + ImmutableList sortedMembersWithComments, VisitorState state) { SuggestedFix.Builder fix = SuggestedFix.builder(); - for (int i = 0; i < members.size(); i++) { - Tree original = members.get(i); - Tree correct = sortedMembers.get(i); - /* Technically not necessary, but avoids redundant replacements. */ - if (!original.equals(correct)) { + for (int i = 0; i < memberWithComments.size(); i++) { + Tree originalMember = memberWithComments.get(i).member(); + MemberWithComments correct = sortedMembersWithComments.get(i); + /* Technically this check is not necessary, but it avoids redundant replacements. */ + if (!originalMember.equals(correct.member())) { String replacement = Stream.concat( - getComments(classTree, correct, state).map(Tokens.Comment::getText), - Stream.of(state.getSourceForNode(correct))) + correct.comments().stream().map(Tokens.Comment::getText), + Stream.of(state.getSourceForNode(correct.member()))) .collect(joining("\n")); - fix.merge(replaceIncludingComments(classTree, original, replacement, state)); + fix.merge( + SuggestedFixes.replaceIncludingComments( + TreePath.getPath(state.getPath(), originalMember), replacement, state)); } } return fix.build(); } - private static boolean isStatic(VariableTree memberTree) { - Set modifiers = memberTree.getModifiers().getFlags(); - return modifiers.contains(Modifier.STATIC); - } - - private static boolean isConstructor(MethodTree methodDecl) { - return ASTHelpers.getSymbol(methodDecl).isConstructor(); - } + // XXX: Work around that `ErrorProneTokens.getTokens(memberSrc, ctx)` returns tokens not + // containing the member's comments. + /** Returns the class' members with their comments. */ + private static ImmutableList getMembersWithComments( + ClassTree classTree, VisitorState state) { + List tokens = + new ArrayList<>( + ErrorProneTokens.getTokens(state.getSourceForNode(classTree), state.context)); - private static Stream getComments( - ClassTree classTree, Tree member, VisitorState state) { - return getTokensBeforeMember(classTree, member, state).findFirst().stream() - .map(ErrorProneToken::comments) - // xxx: Original impl sorts comments, but that seems unnecessary. - .flatMap(List::stream); - } - - // XXX: From this point the code is just a fancy copy of `SuggestFixes.replaceIncludingComments` - - // if we cannot use existing solutions for this functionality, this one needs a big refactor. - - private static SuggestedFix replaceIncludingComments( - ClassTree classTree, Tree member, String replacement, VisitorState state) { - Optional previousMember = getPreviousMember(member, classTree); - ImmutableList tokens = - getTokensBeforeMember(classTree, member, state).collect(toImmutableList()); + ImmutableList.Builder membersWithComments = ImmutableList.builder(); + for (Tree member : classTree.getMembers()) { + ImmutableList memberTokens = + ErrorProneTokens.getTokens(state.getSourceForNode(member), state.context); + if (memberTokens.isEmpty() || memberTokens.get(0).kind() == Tokens.TokenKind.EOF) { + continue; + } - if (tokens.isEmpty()) { - return SuggestedFix.replace(member, replacement); - } - if (tokens.get(0).comments().isEmpty()) { - return SuggestedFix.replace(tokens.get(0).pos(), state.getEndPosition(member), replacement); - } - ImmutableList comments = - ImmutableList.sortedCopyOf( - Comparator.comparingInt(c -> c.getSourcePos(0)).reversed(), - tokens.get(0).comments()); - @Var int startPos = ASTHelpers.getStartPosition(member); - // This can happen for desugared expressions like `int a, b;`. - if (startPos < getStartTokenization(classTree, state, previousMember)) { - return SuggestedFix.emptyFix(); - } - // Delete backwards for comments which are not separated from our target by a blank line. - CharSequence sourceCode = state.getSourceCode(); - for (Tokens.Comment comment : comments) { - int endOfCommentPos = comment.getSourcePos(comment.getText().length() - 1); - CharSequence stringBetweenComments = sourceCode.subSequence(endOfCommentPos, startPos); - if (stringBetweenComments.chars().filter(c -> c == '\n').count() > 1) { - break; + @Var + ImmutableList maybeCommentedMemberTokens = + ImmutableList.copyOf(tokens.subList(0, memberTokens.size())); + while (!areTokenListsMatching(memberTokens, maybeCommentedMemberTokens)) { + tokens.remove(0); + maybeCommentedMemberTokens = ImmutableList.copyOf(tokens.subList(0, memberTokens.size())); } - startPos = comment.getSourcePos(0); + + membersWithComments.add( + new MemberWithComments( + member, ImmutableList.copyOf(maybeCommentedMemberTokens.get(0).comments()))); } - return SuggestedFix.replace(startPos, state.getEndPosition(member), replacement); + return membersWithComments.build(); } - private static Optional getPreviousMember(Tree tree, ClassTree classTree) { - @Var Tree previousMember = null; - for (Tree member : classTree.getMembers()) { - if (member instanceof MethodTree && ASTHelpers.isGeneratedConstructor((MethodTree) member)) { - continue; - } - if (member.equals(tree)) { - break; + /** + * Checks whether two lists of error-prone tokens are 'equal' without considering their comments. + */ + private static boolean areTokenListsMatching( + ImmutableList tokens, ImmutableList memberTokens) { + if (tokens.size() != memberTokens.size()) { + return false; + } + for (int i = 0; i < tokens.size() - 1 /* EOF */; i++) { + if (tokens.get(i).kind() != memberTokens.get(i).kind() + || tokens.get(i).hasName() != memberTokens.get(i).hasName() + || (tokens.get(i).hasName() + && !tokens.get(i).name().equals(memberTokens.get(i).name()))) { + return false; } - previousMember = member; } - return Optional.ofNullable(previousMember); + return true; } - private static Stream getTokensBeforeMember( - ClassTree classTree, Tree member, VisitorState state) { - Optional previousMember = getPreviousMember(member, classTree); - Integer startTokenization = getStartTokenization(classTree, state, previousMember); + private static final class MemberWithComments { + final Tree member; + final ImmutableList comments; - Stream tokens = - state.getOffsetTokens(startTokenization, state.getEndPosition(member)).stream(); + MemberWithComments(Tree member, ImmutableList comments) { + this.member = member; + this.comments = comments; + } - if (previousMember.isEmpty()) { - return tokens.dropWhile(token -> token.kind() != LBRACE).skip(1); + public Tree member() { + return member; } - return tokens; - } - // XXX: rename / remove method - it gets the start position of tokens that *might* be related to - // member. - // - afaik it includes a Class declaration if the member is the first member in the - // class and does not have comments. - private static Integer getStartTokenization( - ClassTree classTree, VisitorState state, Optional previousMember) { - return previousMember - .map(state::getEndPosition) - .orElseGet( - () -> - // XXX: Could return the position of the character next to the opening brace of the - // class - `... Clazz ... {` - // this could make this method more defined, worthy of existence, but it may also - // require additional parameters and changes in `replaceIncludingComments` method. - state.getEndPosition(classTree.getModifiers()) == Position.NOPOS - ? ASTHelpers.getStartPosition(classTree) - : state.getEndPosition(classTree.getModifiers())); + public ImmutableList comments() { + return comments; + } } } diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java index 84e8e1f142..c4a5af1009 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java @@ -1,8 +1,6 @@ package tech.picnic.errorprone.bugpatterns; -import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.containsPattern; -import static com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers.SECOND; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; @@ -15,10 +13,7 @@ void identification() { CompilationTestHelper.newInstance(MemberOrdering.class, getClass()) .expectErrorMessage( "MemberOrdering", - and( - containsPattern("SuppressWarnings"), - containsPattern( - "Members, constructors and methods should follow standard ordering."))) + containsPattern("Members, constructors and methods should follow standard ordering.")) .addSourceLines( "A.java", "// BUG: Diagnostic matches: MemberOrdering", @@ -75,64 +70,6 @@ void identification() { @Test void replacementFirstSuggestedFix() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) - .addInputLines( - "A.java", - "class A {", - " private static final int X = 1;", - " char a = 'a';", - " private static String FOO = \"foo\";", - " static int ONE = 1;", - "", - " void m2() {}", - "", - " public A() {}", - "", - " private static String BAR = \"bar\";", - " char b = 'b';", - "", - " void m1() {", - " System.out.println(\"foo\");", - " }", - "", - " static int TWO = 2;", - "", - " class Inner {}", - "", - " static class StaticInner {}", - "}") - .addOutputLines( - "A.java", - "@SuppressWarnings(\"MemberOrdering\")", - "class A {", - " private static final int X = 1;", - " char a = 'a';", - " private static String FOO = \"foo\";", - " static int ONE = 1;", - "", - " void m2() {}", - "", - " public A() {}", - "", - " private static String BAR = \"bar\";", - " char b = 'b';", - "", - " void m1() {", - " System.out.println(\"foo\");", - " }", - "", - " static int TWO = 2;", - "", - " class Inner {}", - "", - " static class StaticInner {}", - "}") - .doTest(TestMode.TEXT_MATCH); - } - - @Test - void replacementSecondSuggestedFix() { - BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) - .setFixChooser(SECOND) .addInputLines( "A.java", "class A {", @@ -188,9 +125,8 @@ void replacementSecondSuggestedFix() { } @Test - void replacementSecondSuggestedFixConsidersDefaultConstructor() { + void replacementFirstSuggestedFixConsidersDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) - .setFixChooser(SECOND) .addInputLines( "A.java", "class A {", @@ -217,13 +153,13 @@ void replacementSecondSuggestedFixConsidersDefaultConstructor() { } @Test - void replacementSecondSuggestedFixConsidersComments() { + void replacementFirstSuggestedFixConsidersComments() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) - .setFixChooser(SECOND) .addInputLines( "A.java", "class A {", " // `m1()` comment.", + " // `m1()` second comment.", " void m1() {", " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", @@ -241,6 +177,7 @@ void replacementSecondSuggestedFixConsidersComments() { " public A() {}", "", " // `m1()` comment.", + " // `m1()` second comment.", " void m1() {", " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", @@ -249,11 +186,34 @@ void replacementSecondSuggestedFixConsidersComments() { .doTest(TestMode.TEXT_MATCH); } + @Test + void replacementFirstSuggestedFixConsidersAnnotations() { + BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + .addInputLines( + "A.java", + "class A {", + " @SuppressWarnings(\"foo\")", + " void m1() {}", + "", + " @SuppressWarnings(\"bar\")", + " A() {}", + "}") + .addOutputLines( + "A.java", + "class A {", + " @SuppressWarnings(\"bar\")", + " A() {}", + "", + " @SuppressWarnings(\"foo\")", + " void m1() {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test - void replacementSecondSuggestedFixDoesNotModifyWhitespace() { + void replacementFirstSuggestedFixDoesNotModifyWhitespace() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) - .setFixChooser(SECOND) .addInputLines( "A.java", "", @@ -290,12 +250,10 @@ void replacementSecondSuggestedFixDoesNotModifyWhitespace() { .doTest(); } - // todo: Actually verify that whitespace is preserved. + // XXX: This test should fail, if we verify that whitespace is preserved. @SuppressWarnings("ErrorProneTestHelperSourceFormat") - @Test void xxx() { BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) - .setFixChooser(SECOND) .addInputLines( "A.java", "", @@ -337,6 +295,4 @@ void xxx() { "}") .doTest(TestMode.TEXT_MATCH); } - - // todo: Test if second replacement considers annotations. } From dc79300bafd69698a4298d858e57d67e2367245e Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Mon, 10 Jul 2023 08:32:03 +0200 Subject: [PATCH 12/51] Checkstyle --- .../tech/picnic/errorprone/bugpatterns/MemberOrdering.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 370d714517..19b3d72647 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -177,8 +177,8 @@ private static boolean areTokenListsMatching( } private static final class MemberWithComments { - final Tree member; - final ImmutableList comments; + private final Tree member; + private final ImmutableList comments; MemberWithComments(Tree member, ImmutableList comments) { this.member = member; From 76ee9615e3ced0652f07c4ccbbee48d9421c0d83 Mon Sep 17 00:00:00 2001 From: Gijs de Jong Date: Mon, 24 Jul 2023 14:59:50 +0200 Subject: [PATCH 13/51] suggestions --- .../bugpatterns/MemberOrdering.java | 89 ++++++++----------- 1 file changed, 36 insertions(+), 53 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 19b3d72647..4a46620f79 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -12,21 +12,20 @@ import com.google.common.collect.ImmutableList; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; -import com.google.errorprone.annotations.Var; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.fixes.SuggestedFixes; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.ErrorProneToken; -import com.google.errorprone.util.ErrorProneTokens; import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.parser.Tokens; -import java.util.ArrayList; +import com.sun.tools.javac.parser.Tokens.TokenKind; import java.util.Comparator; import java.util.List; import java.util.Set; @@ -36,7 +35,7 @@ /** A {@link BugChecker} that flags classes with non-standard member ordering. */ @AutoService(BugChecker.class) @BugPattern( - summary = "Members should be ordered in a standard way.", + summary = "Members should be ordered in a standard way", explanation = "Members should be ordered in a standard way, which is: " + "static member variables, non-static member variables, constructors and methods.", @@ -50,14 +49,14 @@ public final class MemberOrdering extends BugChecker implements BugChecker.Class /** A comparator that sorts variable and method (incl. constructors) in a standard order. */ private static final Comparator MEMBER_SORTING = comparing( - (Tree memberTree) -> { - switch (memberTree.getKind()) { + (Tree tree) -> { + switch (tree.getKind()) { case VARIABLE: - return isStatic((VariableTree) memberTree) ? 1 : 2; + return isStatic((VariableTree) tree) ? 1 : 2; case METHOD: - return isConstructor((MethodTree) memberTree) ? 3 : 4; + return isConstructor((MethodTree) tree) ? 3 : 4; default: - throw new IllegalStateException("Unexpected kind: " + memberTree.getKind()); + throw new IllegalStateException("Unexpected kind: " + tree.getKind()); } }); @@ -66,12 +65,12 @@ public MemberOrdering() {} @Override public Description matchClass(ClassTree tree, VisitorState state) { - ImmutableList membersWithComments = + ImmutableList membersWithComments = getMembersWithComments(tree, state).stream() - .filter(memberWithComments -> shouldBeSorted(memberWithComments.member())) + .filter(classMemberWithComments -> shouldBeSorted(classMemberWithComments.member())) .collect(toImmutableList()); - ImmutableList sortedMembersWithComments = + ImmutableList sortedMembersWithComments = ImmutableList.sortedCopyOf( (a, b) -> MEMBER_SORTING.compare(a.member(), b.member()), membersWithComments); @@ -103,13 +102,13 @@ private static boolean shouldBeSorted(Tree tree) { } private static SuggestedFix swapMembersWithComments( - ImmutableList memberWithComments, - ImmutableList sortedMembersWithComments, + ImmutableList memberWithComments, + ImmutableList sortedMembersWithComments, VisitorState state) { SuggestedFix.Builder fix = SuggestedFix.builder(); for (int i = 0; i < memberWithComments.size(); i++) { Tree originalMember = memberWithComments.get(i).member(); - MemberWithComments correct = sortedMembersWithComments.get(i); + ClassMemberWithComments correct = sortedMembersWithComments.get(i); /* Technically this check is not necessary, but it avoids redundant replacements. */ if (!originalMember.equals(correct.member())) { String replacement = @@ -125,62 +124,46 @@ private static SuggestedFix swapMembersWithComments( return fix.build(); } - // XXX: Work around that `ErrorProneTokens.getTokens(memberSrc, ctx)` returns tokens not - // containing the member's comments. /** Returns the class' members with their comments. */ - private static ImmutableList getMembersWithComments( + private static ImmutableList getMembersWithComments( ClassTree classTree, VisitorState state) { List tokens = - new ArrayList<>( - ErrorProneTokens.getTokens(state.getSourceForNode(classTree), state.context)); + state.getOffsetTokens( + ASTHelpers.getStartPosition(classTree), state.getEndPosition(classTree)); - ImmutableList.Builder membersWithComments = ImmutableList.builder(); + ImmutableList.Builder membersWithComments = ImmutableList.builder(); for (Tree member : classTree.getMembers()) { - ImmutableList memberTokens = - ErrorProneTokens.getTokens(state.getSourceForNode(member), state.context); - if (memberTokens.isEmpty() || memberTokens.get(0).kind() == Tokens.TokenKind.EOF) { + if (!shouldBeSorted(member)) { continue; } - @Var - ImmutableList maybeCommentedMemberTokens = - ImmutableList.copyOf(tokens.subList(0, memberTokens.size())); - while (!areTokenListsMatching(memberTokens, maybeCommentedMemberTokens)) { - tokens.remove(0); - maybeCommentedMemberTokens = ImmutableList.copyOf(tokens.subList(0, memberTokens.size())); + /* We start at the previous token's end position to cover any possible comments. */ + int memberStartPos = ASTHelpers.getStartPosition(member); + int startPos = + tokens.stream() + .map(ErrorProneToken::endPos) + .filter(i -> i < memberStartPos) + .reduce((first, second) -> second) + .orElse(memberStartPos); + + ImmutableList memberTokens = + ImmutableList.copyOf(state.getOffsetTokens(startPos, state.getEndPosition(member))); + if (memberTokens.isEmpty() || memberTokens.get(0).kind() == TokenKind.EOF) { + continue; } membersWithComments.add( - new MemberWithComments( - member, ImmutableList.copyOf(maybeCommentedMemberTokens.get(0).comments()))); + new ClassMemberWithComments( + member, ImmutableList.copyOf(memberTokens.get(0).comments()))); } return membersWithComments.build(); } - /** - * Checks whether two lists of error-prone tokens are 'equal' without considering their comments. - */ - private static boolean areTokenListsMatching( - ImmutableList tokens, ImmutableList memberTokens) { - if (tokens.size() != memberTokens.size()) { - return false; - } - for (int i = 0; i < tokens.size() - 1 /* EOF */; i++) { - if (tokens.get(i).kind() != memberTokens.get(i).kind() - || tokens.get(i).hasName() != memberTokens.get(i).hasName() - || (tokens.get(i).hasName() - && !tokens.get(i).name().equals(memberTokens.get(i).name()))) { - return false; - } - } - return true; - } - - private static final class MemberWithComments { + private static final class ClassMemberWithComments { private final Tree member; private final ImmutableList comments; - MemberWithComments(Tree member, ImmutableList comments) { + ClassMemberWithComments(Tree member, ImmutableList comments) { this.member = member; this.comments = comments; } From 62823ff830b1817edabae7f5f0e251f4ef467cc9 Mon Sep 17 00:00:00 2001 From: Gijs de Jong Date: Mon, 24 Jul 2023 15:06:37 +0200 Subject: [PATCH 14/51] Remove stray unused import --- .../java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java | 1 - 1 file changed, 1 deletion(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 4a46620f79..982f5a9126 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -21,7 +21,6 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.parser.Tokens; From 7fcb987ae056c90ebaaac62867b01bb095f692c1 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Tue, 25 Jul 2023 17:20:13 +0200 Subject: [PATCH 15/51] Simplifing logic, killing mutants --- .../bugpatterns/MemberOrdering.java | 75 +++++++++++-------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 982f5a9126..72d7d2a8c8 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -1,10 +1,12 @@ package tech.picnic.errorprone.bugpatterns; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.errorprone.BugPattern.LinkType.CUSTOM; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.BugPattern.StandardTags.STYLE; import static java.util.Comparator.comparing; +import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; @@ -24,9 +26,8 @@ import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.parser.Tokens; -import com.sun.tools.javac.parser.Tokens.TokenKind; import java.util.Comparator; -import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.lang.model.element.Modifier; @@ -126,45 +127,53 @@ private static SuggestedFix swapMembersWithComments( /** Returns the class' members with their comments. */ private static ImmutableList getMembersWithComments( ClassTree classTree, VisitorState state) { - List tokens = - state.getOffsetTokens( - ASTHelpers.getStartPosition(classTree), state.getEndPosition(classTree)); - - ImmutableList.Builder membersWithComments = ImmutableList.builder(); - for (Tree member : classTree.getMembers()) { - if (!shouldBeSorted(member)) { - continue; - } - - /* We start at the previous token's end position to cover any possible comments. */ - int memberStartPos = ASTHelpers.getStartPosition(member); - int startPos = - tokens.stream() - .map(ErrorProneToken::endPos) - .filter(i -> i < memberStartPos) - .reduce((first, second) -> second) - .orElse(memberStartPos); - - ImmutableList memberTokens = - ImmutableList.copyOf(state.getOffsetTokens(startPos, state.getEndPosition(member))); - if (memberTokens.isEmpty() || memberTokens.get(0).kind() == TokenKind.EOF) { - continue; - } + return classTree.getMembers().stream() + .map( + member -> + new ClassMemberWithComments(member, getMemberComments(state, classTree, member))) + .collect(toImmutableList()); + } - membersWithComments.add( - new ClassMemberWithComments( - member, ImmutableList.copyOf(memberTokens.get(0).comments()))); + private static ImmutableList getMemberComments( + VisitorState state, ClassTree classTree, Tree member) { + if (member.getKind() == Tree.Kind.METHOD + && ASTHelpers.isGeneratedConstructor((MethodTree) member)) { + return ImmutableList.of(); } - return membersWithComments.build(); + + checkState( + state.getEndPosition(member) != -1, + "Member's end position is not available (-1).\n - member=[%s]\n - source=[%s]", + member, + state.getSourceForNode(member)); + + ImmutableList tokens = + ImmutableList.copyOf( + state.getOffsetTokens( + ASTHelpers.getStartPosition(classTree), state.getEndPosition(classTree))); + + int memberStartPos = ASTHelpers.getStartPosition(member); + Optional previousMemberEndPos = + tokens.stream() + .map(ErrorProneToken::endPos) + .takeWhile(endPos -> endPos <= memberStartPos) + .reduce((earlierPos, laterPos) -> laterPos); + + ImmutableList memberTokens = + ImmutableList.copyOf( + state.getOffsetTokens( + previousMemberEndPos.orElse(memberStartPos), state.getEndPosition(member))); + + return ImmutableList.copyOf(memberTokens.get(0).comments()); } - private static final class ClassMemberWithComments { + static final class ClassMemberWithComments { private final Tree member; private final ImmutableList comments; ClassMemberWithComments(Tree member, ImmutableList comments) { - this.member = member; - this.comments = comments; + this.member = requireNonNull(member); + this.comments = requireNonNull(comments); } public Tree member() { From d01a9591a674e85918db7d92dbccd0782ab2ed20 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Fri, 28 Jul 2023 18:49:35 +0200 Subject: [PATCH 16/51] "Fix" previous token detection --- .../java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index 72d7d2a8c8..e4bb368bcf 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -156,7 +156,7 @@ private static ImmutableList getMemberComments( Optional previousMemberEndPos = tokens.stream() .map(ErrorProneToken::endPos) - .takeWhile(endPos -> endPos <= memberStartPos) + .takeWhile(endPos -> endPos < memberStartPos) .reduce((earlierPos, laterPos) -> laterPos); ImmutableList memberTokens = From c12e2e02886a97edf0ab47251492475af9c48a57 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Fri, 28 Jul 2023 18:50:15 +0200 Subject: [PATCH 17/51] Assume that only generated members' end position is unavailable. --- .../errorprone/bugpatterns/MemberOrdering.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java index e4bb368bcf..aecb33a4a9 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java @@ -1,6 +1,5 @@ package tech.picnic.errorprone.bugpatterns; -import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.errorprone.BugPattern.LinkType.CUSTOM; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; @@ -136,17 +135,12 @@ private static ImmutableList getMembersWithComments( private static ImmutableList getMemberComments( VisitorState state, ClassTree classTree, Tree member) { - if (member.getKind() == Tree.Kind.METHOD - && ASTHelpers.isGeneratedConstructor((MethodTree) member)) { + if (state.getEndPosition(member) == -1) { + // Member is probably generated, according to `VisitorState` its end position is "not + // available". return ImmutableList.of(); } - checkState( - state.getEndPosition(member) != -1, - "Member's end position is not available (-1).\n - member=[%s]\n - source=[%s]", - member, - state.getSourceForNode(member)); - ImmutableList tokens = ImmutableList.copyOf( state.getOffsetTokens( From 381b063f91d0e456d7b6f6da2c27221178f94313 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sat, 29 Jul 2023 14:19:31 +0200 Subject: [PATCH 18/51] Suggestions on naming, minor organizational changes, rewordings --- .../bugpatterns/ClassMemberOrdering.java | 188 ++++++++++++++++++ .../bugpatterns/MemberOrdering.java | 181 ----------------- ...Test.java => ClassMemberOrderingTest.java} | 22 +- 3 files changed, 199 insertions(+), 192 deletions(-) create mode 100644 error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java delete mode 100644 error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java rename error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/{MemberOrderingTest.java => ClassMemberOrderingTest.java} (90%) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java new file mode 100644 index 0000000000..f8a59474c9 --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java @@ -0,0 +1,188 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.errorprone.BugPattern.LinkType.CUSTOM; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.BugPattern.StandardTags.STYLE; +import static java.util.Comparator.comparing; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; +import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; + +import com.google.auto.service.AutoService; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.fixes.SuggestedFixes; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; +import com.google.errorprone.util.ErrorProneToken; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; +import com.sun.tools.javac.parser.Tokens; +import java.util.Comparator; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import javax.lang.model.element.Modifier; + +/** A {@link BugChecker} that flags classes with non-standard member ordering. */ +@AutoService(BugChecker.class) +@BugPattern( + summary = "Class members should be ordered in a standard way", + explanation = + "Class members should be ordered in a standard way, which is: " + + "static fields, non-static fields, constructors and methods.", + link = BUG_PATTERNS_BASE_URL + "ClassMemberOrdering", + linkType = CUSTOM, + severity = WARNING, + tags = STYLE) +public final class ClassMemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { + private static final long serialVersionUID = 1L; + + /** A comparator that sorts class members (including constructors) in a standard order. */ + private static final Comparator CLASS_MEMBER_SORTER = + comparing( + (Tree tree) -> { + switch (tree.getKind()) { + case VARIABLE: + return isStatic((VariableTree) tree) ? 1 : 2; + case METHOD: + return isConstructor((MethodTree) tree) ? 3 : 4; + default: + throw new IllegalStateException("Unexpected kind: " + tree.getKind()); + } + }); + + /** Instantiates a new {@link ClassMemberOrdering} instance. */ + public ClassMemberOrdering() {} + + @Override + public Description matchClass(ClassTree classTree, VisitorState state) { + ImmutableList classMembers = + getClassMembersWithComments(classTree, state).stream() + .filter(classMember -> shouldBeSorted(classMember.tree())) + .collect(toImmutableList()); + + ImmutableList sortedClassMembers = + ImmutableList.sortedCopyOf( + (a, b) -> CLASS_MEMBER_SORTER.compare(a.tree(), b.tree()), classMembers); + + if (classMembers.equals(sortedClassMembers)) { + return Description.NO_MATCH; + } + + return buildDescription(classTree) + .addFix(replaceClassMembers(classMembers, sortedClassMembers, state)) + .setMessage( + "Fields, constructors and methods should follow standard ordering. " + + "The standard ordering is: static fields, non-static fields, " + + "constructors and methods.") + .build(); + } + + private static boolean isStatic(VariableTree variableTree) { + Set modifiers = variableTree.getModifiers().getFlags(); + return modifiers.contains(Modifier.STATIC); + } + + private static boolean isConstructor(MethodTree methodTree) { + return ASTHelpers.getSymbol(methodTree).isConstructor(); + } + + private static boolean shouldBeSorted(Tree tree) { + return tree instanceof VariableTree + || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)); + } + + private static SuggestedFix replaceClassMembers( + ImmutableList classMembers, + ImmutableList replacementClassMembers, + VisitorState state) { + SuggestedFix.Builder fix = SuggestedFix.builder(); + for (int i = 0; i < classMembers.size(); i++) { + ClassMemberWithComments original = classMembers.get(i); + ClassMemberWithComments replacement = replacementClassMembers.get(i); + fix.merge(replaceClassMember(state, original, replacement)); + } + return fix.build(); + } + + private static SuggestedFix replaceClassMember( + VisitorState state, ClassMemberWithComments original, ClassMemberWithComments replacement) { + /* Technically this check is not necessary, but it avoids redundant replacements. */ + if (original.equals(replacement)) { + return SuggestedFix.emptyFix(); + } + String replacementSource = + Stream.concat( + replacement.comments().stream().map(Tokens.Comment::getText), + Stream.of(state.getSourceForNode(replacement.tree()))) + .collect(joining("\n")); + return SuggestedFixes.replaceIncludingComments( + TreePath.getPath(state.getPath(), original.tree()), replacementSource, state); + } + + /** Returns the class' members with their comments. */ + private static ImmutableList getClassMembersWithComments( + ClassTree classTree, VisitorState state) { + return classTree.getMembers().stream() + .map( + classMember -> + new ClassMemberWithComments( + classMember, getClassMemberComments(state, classTree, classMember))) + .collect(toImmutableList()); + } + + private static ImmutableList getClassMemberComments( + VisitorState state, ClassTree classTree, Tree classMember) { + if (state.getEndPosition(classMember) == -1) { + // Member is probably generated, according to `VisitorState` its end position is "not + // available". + return ImmutableList.of(); + } + + ImmutableList classTokens = + ImmutableList.copyOf( + state.getOffsetTokens( + ASTHelpers.getStartPosition(classTree), state.getEndPosition(classTree))); + + int classMemberStartPos = ASTHelpers.getStartPosition(classMember); + Optional previousClassTokenEndPos = + classTokens.stream() + .map(ErrorProneToken::endPos) + .takeWhile(endPos -> endPos < classMemberStartPos) + .reduce((earlierPos, laterPos) -> laterPos); + + ImmutableList classMemberTokens = + ImmutableList.copyOf( + state.getOffsetTokens( + previousClassTokenEndPos.orElse(classMemberStartPos), + state.getEndPosition(classMember))); + + return ImmutableList.copyOf(classMemberTokens.get(0).comments()); + } + + private static final class ClassMemberWithComments { + private final Tree tree; + private final ImmutableList comments; + + ClassMemberWithComments(Tree tree, ImmutableList comments) { + this.tree = requireNonNull(tree); + this.comments = requireNonNull(comments); + } + + public Tree tree() { + return tree; + } + + public ImmutableList comments() { + return comments; + } + } +} diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java deleted file mode 100644 index aecb33a4a9..0000000000 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/MemberOrdering.java +++ /dev/null @@ -1,181 +0,0 @@ -package tech.picnic.errorprone.bugpatterns; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.errorprone.BugPattern.LinkType.CUSTOM; -import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; -import static com.google.errorprone.BugPattern.StandardTags.STYLE; -import static java.util.Comparator.comparing; -import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.joining; -import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; - -import com.google.auto.service.AutoService; -import com.google.common.collect.ImmutableList; -import com.google.errorprone.BugPattern; -import com.google.errorprone.VisitorState; -import com.google.errorprone.bugpatterns.BugChecker; -import com.google.errorprone.fixes.SuggestedFix; -import com.google.errorprone.fixes.SuggestedFixes; -import com.google.errorprone.matchers.Description; -import com.google.errorprone.util.ASTHelpers; -import com.google.errorprone.util.ErrorProneToken; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.VariableTree; -import com.sun.source.util.TreePath; -import com.sun.tools.javac.parser.Tokens; -import java.util.Comparator; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; -import javax.lang.model.element.Modifier; - -/** A {@link BugChecker} that flags classes with non-standard member ordering. */ -@AutoService(BugChecker.class) -@BugPattern( - summary = "Members should be ordered in a standard way", - explanation = - "Members should be ordered in a standard way, which is: " - + "static member variables, non-static member variables, constructors and methods.", - link = BUG_PATTERNS_BASE_URL + "MemberOrdering", - linkType = CUSTOM, - severity = WARNING, - tags = STYLE) -public final class MemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { - private static final long serialVersionUID = 1L; - - /** A comparator that sorts variable and method (incl. constructors) in a standard order. */ - private static final Comparator MEMBER_SORTING = - comparing( - (Tree tree) -> { - switch (tree.getKind()) { - case VARIABLE: - return isStatic((VariableTree) tree) ? 1 : 2; - case METHOD: - return isConstructor((MethodTree) tree) ? 3 : 4; - default: - throw new IllegalStateException("Unexpected kind: " + tree.getKind()); - } - }); - - /** Instantiates a new {@link MemberOrdering} instance. */ - public MemberOrdering() {} - - @Override - public Description matchClass(ClassTree tree, VisitorState state) { - ImmutableList membersWithComments = - getMembersWithComments(tree, state).stream() - .filter(classMemberWithComments -> shouldBeSorted(classMemberWithComments.member())) - .collect(toImmutableList()); - - ImmutableList sortedMembersWithComments = - ImmutableList.sortedCopyOf( - (a, b) -> MEMBER_SORTING.compare(a.member(), b.member()), membersWithComments); - - if (membersWithComments.equals(sortedMembersWithComments)) { - return Description.NO_MATCH; - } - - return buildDescription(tree) - .addFix(swapMembersWithComments(membersWithComments, sortedMembersWithComments, state)) - .setMessage( - "Members, constructors and methods should follow standard ordering. " - + "The standard ordering is: static variables, non-static variables, " - + "constructors and methods.") - .build(); - } - - private static boolean isStatic(VariableTree memberTree) { - Set modifiers = memberTree.getModifiers().getFlags(); - return modifiers.contains(Modifier.STATIC); - } - - private static boolean isConstructor(MethodTree methodDecl) { - return ASTHelpers.getSymbol(methodDecl).isConstructor(); - } - - private static boolean shouldBeSorted(Tree tree) { - return tree instanceof VariableTree - || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)); - } - - private static SuggestedFix swapMembersWithComments( - ImmutableList memberWithComments, - ImmutableList sortedMembersWithComments, - VisitorState state) { - SuggestedFix.Builder fix = SuggestedFix.builder(); - for (int i = 0; i < memberWithComments.size(); i++) { - Tree originalMember = memberWithComments.get(i).member(); - ClassMemberWithComments correct = sortedMembersWithComments.get(i); - /* Technically this check is not necessary, but it avoids redundant replacements. */ - if (!originalMember.equals(correct.member())) { - String replacement = - Stream.concat( - correct.comments().stream().map(Tokens.Comment::getText), - Stream.of(state.getSourceForNode(correct.member()))) - .collect(joining("\n")); - fix.merge( - SuggestedFixes.replaceIncludingComments( - TreePath.getPath(state.getPath(), originalMember), replacement, state)); - } - } - return fix.build(); - } - - /** Returns the class' members with their comments. */ - private static ImmutableList getMembersWithComments( - ClassTree classTree, VisitorState state) { - return classTree.getMembers().stream() - .map( - member -> - new ClassMemberWithComments(member, getMemberComments(state, classTree, member))) - .collect(toImmutableList()); - } - - private static ImmutableList getMemberComments( - VisitorState state, ClassTree classTree, Tree member) { - if (state.getEndPosition(member) == -1) { - // Member is probably generated, according to `VisitorState` its end position is "not - // available". - return ImmutableList.of(); - } - - ImmutableList tokens = - ImmutableList.copyOf( - state.getOffsetTokens( - ASTHelpers.getStartPosition(classTree), state.getEndPosition(classTree))); - - int memberStartPos = ASTHelpers.getStartPosition(member); - Optional previousMemberEndPos = - tokens.stream() - .map(ErrorProneToken::endPos) - .takeWhile(endPos -> endPos < memberStartPos) - .reduce((earlierPos, laterPos) -> laterPos); - - ImmutableList memberTokens = - ImmutableList.copyOf( - state.getOffsetTokens( - previousMemberEndPos.orElse(memberStartPos), state.getEndPosition(member))); - - return ImmutableList.copyOf(memberTokens.get(0).comments()); - } - - static final class ClassMemberWithComments { - private final Tree member; - private final ImmutableList comments; - - ClassMemberWithComments(Tree member, ImmutableList comments) { - this.member = requireNonNull(member); - this.comments = requireNonNull(comments); - } - - public Tree member() { - return member; - } - - public ImmutableList comments() { - return comments; - } - } -} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java similarity index 90% rename from error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java rename to error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java index c4a5af1009..676963f958 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/MemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java @@ -7,16 +7,16 @@ import com.google.errorprone.CompilationTestHelper; import org.junit.jupiter.api.Test; -final class MemberOrderingTest { +final class ClassMemberOrderingTest { @Test void identification() { - CompilationTestHelper.newInstance(MemberOrdering.class, getClass()) + CompilationTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .expectErrorMessage( - "MemberOrdering", - containsPattern("Members, constructors and methods should follow standard ordering.")) + "ClassMemberOrdering", + containsPattern("Fields, constructors and methods should follow standard ordering.")) .addSourceLines( "A.java", - "// BUG: Diagnostic matches: MemberOrdering", + "// BUG: Diagnostic matches: ClassMemberOrdering", "class A {", " char a = 'a';", " private static String FOO = \"foo\";", @@ -69,7 +69,7 @@ void identification() { @Test void replacementFirstSuggestedFix() { - BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -126,7 +126,7 @@ void replacementFirstSuggestedFix() { @Test void replacementFirstSuggestedFixConsidersDefaultConstructor() { - BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -154,7 +154,7 @@ void replacementFirstSuggestedFixConsidersDefaultConstructor() { @Test void replacementFirstSuggestedFixConsidersComments() { - BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -188,7 +188,7 @@ void replacementFirstSuggestedFixConsidersComments() { @Test void replacementFirstSuggestedFixConsidersAnnotations() { - BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -213,7 +213,7 @@ void replacementFirstSuggestedFixConsidersAnnotations() { @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementFirstSuggestedFixDoesNotModifyWhitespace() { - BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .addInputLines( "A.java", "", @@ -253,7 +253,7 @@ void replacementFirstSuggestedFixDoesNotModifyWhitespace() { // XXX: This test should fail, if we verify that whitespace is preserved. @SuppressWarnings("ErrorProneTestHelperSourceFormat") void xxx() { - BugCheckerRefactoringTestHelper.newInstance(MemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .addInputLines( "A.java", "", From a47b718f69b71d77757d9a580fa57de168aa1052 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sat, 29 Jul 2023 17:08:11 +0200 Subject: [PATCH 19/51] Reproduce error-prone token start - end position overlap --- .../bugpatterns/ClassMemberOrderingTest.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java index 676963f958..d279ea2193 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java @@ -152,15 +152,19 @@ void replacementFirstSuggestedFixConsidersDefaultConstructor() { .doTest(TestMode.TEXT_MATCH); } + @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementFirstSuggestedFixConsidersComments() { BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", - " // `m1()` comment.", - " // `m1()` second comment.", - " void m1() {", + " // detached comment from method", + " ;void method1() {}", + "", + " // first comment prior to method", + " // second comment prior to method", + " void method2() {", " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", " }", @@ -176,9 +180,12 @@ void replacementFirstSuggestedFixConsidersComments() { " /** Instantiates a new {@link A} instance. */", " public A() {}", "", - " // `m1()` comment.", - " // `m1()` second comment.", - " void m1() {", + " // detached comment from method", + " void method1() {}", + "", + " // first comment prior to method", + " // second comment prior to method", + " void method2() {", " // Print line 'foo' to stdout.", " System.out.println(\"foo\");", " }", From 156c2bab34aea4f593213f5023fdc4a5dda12a31 Mon Sep 17 00:00:00 2001 From: Stephan Schroevers Date: Sat, 12 Aug 2023 19:17:52 +0200 Subject: [PATCH 20/51] Suggestions --- .../bugpatterns/ClassMemberOrdering.java | 97 +++++++++---------- 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java index f8a59474c9..cde2a737e0 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java @@ -5,12 +5,13 @@ import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.BugPattern.StandardTags.STYLE; import static java.util.Comparator.comparing; -import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.joining; import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; @@ -25,13 +26,18 @@ import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.parser.Tokens; +import com.sun.tools.javac.util.Position; import java.util.Comparator; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javax.lang.model.element.Modifier; +import tech.picnic.errorprone.bugpatterns.util.SourceCode; /** A {@link BugChecker} that flags classes with non-standard member ordering. */ +// XXX: Reference +// https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html @AutoService(BugChecker.class) @BugPattern( summary = "Class members should be ordered in a standard way", @@ -44,11 +50,10 @@ tags = STYLE) public final class ClassMemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { private static final long serialVersionUID = 1L; - - /** A comparator that sorts class members (including constructors) in a standard order. */ - private static final Comparator CLASS_MEMBER_SORTER = + /** Orders {@link Tree}s to match the standard Java type member declaration order. */ + private static final Comparator BY_PREFERRED_TYPE_MEMBER_ORDER = comparing( - (Tree tree) -> { + tree -> { switch (tree.getKind()) { case VARIABLE: return isStatic((VariableTree) tree) ? 1 : 2; @@ -71,7 +76,7 @@ public Description matchClass(ClassTree classTree, VisitorState state) { ImmutableList sortedClassMembers = ImmutableList.sortedCopyOf( - (a, b) -> CLASS_MEMBER_SORTER.compare(a.tree(), b.tree()), classMembers); + (a, b) -> BY_PREFERRED_TYPE_MEMBER_ORDER.compare(a.tree(), b.tree()), classMembers); if (classMembers.equals(sortedClassMembers)) { return Description.NO_MATCH; @@ -104,13 +109,12 @@ private static SuggestedFix replaceClassMembers( ImmutableList classMembers, ImmutableList replacementClassMembers, VisitorState state) { - SuggestedFix.Builder fix = SuggestedFix.builder(); - for (int i = 0; i < classMembers.size(); i++) { - ClassMemberWithComments original = classMembers.get(i); - ClassMemberWithComments replacement = replacementClassMembers.get(i); - fix.merge(replaceClassMember(state, original, replacement)); - } - return fix.build(); + return Streams.zip( + classMembers.stream(), + replacementClassMembers.stream(), + (original, replacement) -> replaceClassMember(state, original, replacement)) + .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) + .build(); } private static SuggestedFix replaceClassMember( @@ -119,11 +123,12 @@ private static SuggestedFix replaceClassMember( if (original.equals(replacement)) { return SuggestedFix.emptyFix(); } + String replacementSource = Stream.concat( - replacement.comments().stream().map(Tokens.Comment::getText), - Stream.of(state.getSourceForNode(replacement.tree()))) - .collect(joining("\n")); + replacement.comments().stream(), + Stream.of(SourceCode.treeToString(replacement.tree(), state))) + .collect(joining(System.lineSeparator())); return SuggestedFixes.replaceIncludingComments( TreePath.getPath(state.getPath(), original.tree()), replacementSource, state); } @@ -133,56 +138,44 @@ private static ImmutableList getClassMembersWithComment ClassTree classTree, VisitorState state) { return classTree.getMembers().stream() .map( - classMember -> - new ClassMemberWithComments( - classMember, getClassMemberComments(state, classTree, classMember))) + member -> + new AutoValue_ClassMemberOrdering_ClassMemberWithComments( + member, getClassMemberComments(state, classTree, member))) .collect(toImmutableList()); } - private static ImmutableList getClassMemberComments( + private static ImmutableList getClassMemberComments( VisitorState state, ClassTree classTree, Tree classMember) { - if (state.getEndPosition(classMember) == -1) { - // Member is probably generated, according to `VisitorState` its end position is "not - // available". + int typeStart = ASTHelpers.getStartPosition(classTree); + int typeEnd = state.getEndPosition(classTree); + int memberStart = ASTHelpers.getStartPosition(classMember); + int memberEnd = state.getEndPosition(classMember); + if (typeStart == Position.NOPOS + || typeEnd == Position.NOPOS + || memberStart == Position.NOPOS + || memberEnd == Position.NOPOS) { + /* Source code details appear to be unavailable. */ return ImmutableList.of(); } - ImmutableList classTokens = - ImmutableList.copyOf( - state.getOffsetTokens( - ASTHelpers.getStartPosition(classTree), state.getEndPosition(classTree))); - - int classMemberStartPos = ASTHelpers.getStartPosition(classMember); Optional previousClassTokenEndPos = - classTokens.stream() + state.getOffsetTokens(typeStart, typeEnd).stream() .map(ErrorProneToken::endPos) - .takeWhile(endPos -> endPos < classMemberStartPos) + .takeWhile(endPos -> endPos < memberStart) .reduce((earlierPos, laterPos) -> laterPos); - ImmutableList classMemberTokens = - ImmutableList.copyOf( - state.getOffsetTokens( - previousClassTokenEndPos.orElse(classMemberStartPos), - state.getEndPosition(classMember))); + List classMemberTokens = + state.getOffsetTokens(previousClassTokenEndPos.orElse(memberStart), memberEnd); - return ImmutableList.copyOf(classMemberTokens.get(0).comments()); + return classMemberTokens.get(0).comments().stream() + .map(Tokens.Comment::getText) + .collect(toImmutableList()); } - private static final class ClassMemberWithComments { - private final Tree tree; - private final ImmutableList comments; + @AutoValue + abstract static class ClassMemberWithComments { + abstract Tree tree(); - ClassMemberWithComments(Tree tree, ImmutableList comments) { - this.tree = requireNonNull(tree); - this.comments = requireNonNull(comments); - } - - public Tree tree() { - return tree; - } - - public ImmutableList comments() { - return comments; - } + abstract ImmutableList comments(); } } From 197eebaae0db4a337951aff6d418f7e79398a6b5 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 13:52:25 +0100 Subject: [PATCH 21/51] List all remarks as TODOs, or address them --- ...rOrdering.java => TypeMemberOrdering.java} | 26 +++++++++++-------- .../bugpatterns/ClassMemberOrderingTest.java | 6 +++++ 2 files changed, 21 insertions(+), 11 deletions(-) rename error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/{ClassMemberOrdering.java => TypeMemberOrdering.java} (90%) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java similarity index 90% rename from error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java rename to error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java index cde2a737e0..eef586897c 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java @@ -35,21 +35,25 @@ import javax.lang.model.element.Modifier; import tech.picnic.errorprone.bugpatterns.util.SourceCode; -/** A {@link BugChecker} that flags classes with non-standard member ordering. */ +/** + * A {@link BugChecker} that flags classes with non-standard member ordering. + * + *

Type members should be ordered in a standard way, which is: static fields, non-static fields, + * constructors and methods. + */ // XXX: Reference // https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html @AutoService(BugChecker.class) @BugPattern( summary = "Class members should be ordered in a standard way", - explanation = - "Class members should be ordered in a standard way, which is: " - + "static fields, non-static fields, constructors and methods.", link = BUG_PATTERNS_BASE_URL + "ClassMemberOrdering", linkType = CUSTOM, severity = WARNING, tags = STYLE) public final class ClassMemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { private static final long serialVersionUID = 1L; + + // TODO: Copy should be sorted and comparator in-sync. /** Orders {@link Tree}s to match the standard Java type member declaration order. */ private static final Comparator BY_PREFERRED_TYPE_MEMBER_ORDER = comparing( @@ -76,7 +80,7 @@ public Description matchClass(ClassTree classTree, VisitorState state) { ImmutableList sortedClassMembers = ImmutableList.sortedCopyOf( - (a, b) -> BY_PREFERRED_TYPE_MEMBER_ORDER.compare(a.tree(), b.tree()), classMembers); + comparing(ClassMemberWithComments::tree, BY_PREFERRED_TYPE_MEMBER_ORDER), classMembers); if (classMembers.equals(sortedClassMembers)) { return Description.NO_MATCH; @@ -84,10 +88,6 @@ public Description matchClass(ClassTree classTree, VisitorState state) { return buildDescription(classTree) .addFix(replaceClassMembers(classMembers, sortedClassMembers, state)) - .setMessage( - "Fields, constructors and methods should follow standard ordering. " - + "The standard ordering is: static fields, non-static fields, " - + "constructors and methods.") .build(); } @@ -158,15 +158,19 @@ private static ImmutableList getClassMemberComments( return ImmutableList.of(); } - Optional previousClassTokenEndPos = + // TODO: Move identifying "previous member end position" to an outer loop, + // Loop once and identify for all members + // TODO: Check if this handles properly comments on the first member. + Optional previousMemberEndPos = state.getOffsetTokens(typeStart, typeEnd).stream() .map(ErrorProneToken::endPos) .takeWhile(endPos -> endPos < memberStart) .reduce((earlierPos, laterPos) -> laterPos); List classMemberTokens = - state.getOffsetTokens(previousClassTokenEndPos.orElse(memberStart), memberEnd); + state.getOffsetTokens(previousMemberEndPos.orElse(memberStart), memberEnd); + // TODO: double check this .get(0) return classMemberTokens.get(0).comments().stream() .map(Tokens.Comment::getText) .collect(toImmutableList()); diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java index d279ea2193..d5964031a1 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java @@ -7,6 +7,12 @@ import com.google.errorprone.CompilationTestHelper; import org.junit.jupiter.api.Test; +/* +TODO: Order static and non-static initializer blocks. +TODO: Skip classes / members annotated with @SuppressWarnings() all or classMemberOrdering +TODO: Rename Class* to Type* -- this can apply also to interfaces or records +TODO: Handle inner types +*/ final class ClassMemberOrderingTest { @Test void identification() { From 4bbb603392eabaf540f65600aa38134239172389 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 14:06:59 +0100 Subject: [PATCH 22/51] Rename `*class*` to `*type*` --- .../bugpatterns/TypeMemberOrdering.java | 64 +++++++++---------- ...gTest.java => TypeMemberOrderingTest.java} | 24 +++---- 2 files changed, 44 insertions(+), 44 deletions(-) rename error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/{ClassMemberOrderingTest.java => TypeMemberOrderingTest.java} (90%) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java index eef586897c..ce8298f389 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java @@ -36,7 +36,7 @@ import tech.picnic.errorprone.bugpatterns.util.SourceCode; /** - * A {@link BugChecker} that flags classes with non-standard member ordering. + * A {@link BugChecker} that flags types with non-standard member ordering. * *

Type members should be ordered in a standard way, which is: static fields, non-static fields, * constructors and methods. @@ -45,12 +45,12 @@ // https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html @AutoService(BugChecker.class) @BugPattern( - summary = "Class members should be ordered in a standard way", - link = BUG_PATTERNS_BASE_URL + "ClassMemberOrdering", + summary = "Type members should be ordered in a standard way", + link = BUG_PATTERNS_BASE_URL + "TypeMemberOrdering", linkType = CUSTOM, severity = WARNING, tags = STYLE) -public final class ClassMemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { +public final class TypeMemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { private static final long serialVersionUID = 1L; // TODO: Copy should be sorted and comparator in-sync. @@ -68,26 +68,26 @@ public final class ClassMemberOrdering extends BugChecker implements BugChecker. } }); - /** Instantiates a new {@link ClassMemberOrdering} instance. */ - public ClassMemberOrdering() {} + /** Instantiates a new {@link TypeMemberOrdering} instance. */ + public TypeMemberOrdering() {} @Override public Description matchClass(ClassTree classTree, VisitorState state) { - ImmutableList classMembers = - getClassMembersWithComments(classTree, state).stream() - .filter(classMember -> shouldBeSorted(classMember.tree())) + ImmutableList typeMembers = + getTypeMembersWithComments(classTree, state).stream() + .filter(typeMember -> shouldBeSorted(typeMember.tree())) .collect(toImmutableList()); - ImmutableList sortedClassMembers = + ImmutableList sortedTypeMembers = ImmutableList.sortedCopyOf( - comparing(ClassMemberWithComments::tree, BY_PREFERRED_TYPE_MEMBER_ORDER), classMembers); + comparing(TypeMemberWithComments::tree, BY_PREFERRED_TYPE_MEMBER_ORDER), typeMembers); - if (classMembers.equals(sortedClassMembers)) { + if (typeMembers.equals(sortedTypeMembers)) { return Description.NO_MATCH; } return buildDescription(classTree) - .addFix(replaceClassMembers(classMembers, sortedClassMembers, state)) + .addFix(replaceTypeMembers(typeMembers, sortedTypeMembers, state)) .build(); } @@ -105,20 +105,20 @@ private static boolean shouldBeSorted(Tree tree) { || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)); } - private static SuggestedFix replaceClassMembers( - ImmutableList classMembers, - ImmutableList replacementClassMembers, + private static SuggestedFix replaceTypeMembers( + ImmutableList typeMembers, + ImmutableList replacementTypeMembers, VisitorState state) { return Streams.zip( - classMembers.stream(), - replacementClassMembers.stream(), - (original, replacement) -> replaceClassMember(state, original, replacement)) + typeMembers.stream(), + replacementTypeMembers.stream(), + (original, replacement) -> replaceTypeMember(state, original, replacement)) .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) .build(); } - private static SuggestedFix replaceClassMember( - VisitorState state, ClassMemberWithComments original, ClassMemberWithComments replacement) { + private static SuggestedFix replaceTypeMember( + VisitorState state, TypeMemberWithComments original, TypeMemberWithComments replacement) { /* Technically this check is not necessary, but it avoids redundant replacements. */ if (original.equals(replacement)) { return SuggestedFix.emptyFix(); @@ -133,23 +133,23 @@ private static SuggestedFix replaceClassMember( TreePath.getPath(state.getPath(), original.tree()), replacementSource, state); } - /** Returns the class' members with their comments. */ - private static ImmutableList getClassMembersWithComments( + /** Returns the type's members with their comments. */ + private static ImmutableList getTypeMembersWithComments( ClassTree classTree, VisitorState state) { return classTree.getMembers().stream() .map( member -> - new AutoValue_ClassMemberOrdering_ClassMemberWithComments( - member, getClassMemberComments(state, classTree, member))) + new AutoValue_TypeMemberOrdering_TypeMemberWithComments( + member, getTypeMemberComments(state, classTree, member))) .collect(toImmutableList()); } - private static ImmutableList getClassMemberComments( - VisitorState state, ClassTree classTree, Tree classMember) { + private static ImmutableList getTypeMemberComments( + VisitorState state, ClassTree classTree, Tree member) { int typeStart = ASTHelpers.getStartPosition(classTree); int typeEnd = state.getEndPosition(classTree); - int memberStart = ASTHelpers.getStartPosition(classMember); - int memberEnd = state.getEndPosition(classMember); + int memberStart = ASTHelpers.getStartPosition(member); + int memberEnd = state.getEndPosition(member); if (typeStart == Position.NOPOS || typeEnd == Position.NOPOS || memberStart == Position.NOPOS @@ -167,17 +167,17 @@ private static ImmutableList getClassMemberComments( .takeWhile(endPos -> endPos < memberStart) .reduce((earlierPos, laterPos) -> laterPos); - List classMemberTokens = + List typeMemberTokens = state.getOffsetTokens(previousMemberEndPos.orElse(memberStart), memberEnd); // TODO: double check this .get(0) - return classMemberTokens.get(0).comments().stream() + return typeMemberTokens.get(0).comments().stream() .map(Tokens.Comment::getText) .collect(toImmutableList()); } @AutoValue - abstract static class ClassMemberWithComments { + abstract static class TypeMemberWithComments { abstract Tree tree(); abstract ImmutableList comments(); diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java similarity index 90% rename from error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java rename to error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index d5964031a1..be00842440 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/ClassMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -9,20 +9,20 @@ /* TODO: Order static and non-static initializer blocks. -TODO: Skip classes / members annotated with @SuppressWarnings() all or classMemberOrdering -TODO: Rename Class* to Type* -- this can apply also to interfaces or records +TODO: Skip types / members annotated with @SuppressWarnings() all or typeMemberOrdering +TODO: Add tests for interfaces and records TODO: Handle inner types */ -final class ClassMemberOrderingTest { +final class TypeMemberOrderingTest { @Test void identification() { - CompilationTestHelper.newInstance(ClassMemberOrdering.class, getClass()) + CompilationTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .expectErrorMessage( - "ClassMemberOrdering", + "TypeMemberOrdering", containsPattern("Fields, constructors and methods should follow standard ordering.")) .addSourceLines( "A.java", - "// BUG: Diagnostic matches: ClassMemberOrdering", + "// BUG: Diagnostic matches: TypeMemberOrdering", "class A {", " char a = 'a';", " private static String FOO = \"foo\";", @@ -75,7 +75,7 @@ void identification() { @Test void replacementFirstSuggestedFix() { - BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -132,7 +132,7 @@ void replacementFirstSuggestedFix() { @Test void replacementFirstSuggestedFixConsidersDefaultConstructor() { - BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -161,7 +161,7 @@ void replacementFirstSuggestedFixConsidersDefaultConstructor() { @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementFirstSuggestedFixConsidersComments() { - BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -201,7 +201,7 @@ void replacementFirstSuggestedFixConsidersComments() { @Test void replacementFirstSuggestedFixConsidersAnnotations() { - BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", "class A {", @@ -226,7 +226,7 @@ void replacementFirstSuggestedFixConsidersAnnotations() { @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test void replacementFirstSuggestedFixDoesNotModifyWhitespace() { - BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", "", @@ -266,7 +266,7 @@ void replacementFirstSuggestedFixDoesNotModifyWhitespace() { // XXX: This test should fail, if we verify that whitespace is preserved. @SuppressWarnings("ErrorProneTestHelperSourceFormat") void xxx() { - BugCheckerRefactoringTestHelper.newInstance(ClassMemberOrdering.class, getClass()) + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", "", From 909449d14e008db02c9973ea36e1f9b53fe2e6f0 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 14:20:19 +0100 Subject: [PATCH 23/51] Add identification test for an empty class, sync identifaction test w/ new error message --- .../errorprone/bugpatterns/TypeMemberOrderingTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index be00842440..34951fbe66 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -19,7 +19,7 @@ void identification() { CompilationTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .expectErrorMessage( "TypeMemberOrdering", - containsPattern("Fields, constructors and methods should follow standard ordering.")) + containsPattern("Type members should be ordered in a standard way")) .addSourceLines( "A.java", "// BUG: Diagnostic matches: TypeMemberOrdering", @@ -70,6 +70,10 @@ void identification() { "", " static class StaticInner {}", "}") + .addSourceLines( + "Empty.java", + "class Empty {", + "}") .doTest(); } From 1a306a6fdb0404a298c1646ea2699eefcbc94c93 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 14:27:44 +0100 Subject: [PATCH 24/51] Verify that SupressWarnings("all" or "TypeMemberOrdering") are not ordered --- .../bugpatterns/TypeMemberOrderingTest.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index 34951fbe66..f2a8012370 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -9,7 +9,7 @@ /* TODO: Order static and non-static initializer blocks. -TODO: Skip types / members annotated with @SuppressWarnings() all or typeMemberOrdering +TODO: Skip members annotated with @SuppressWarnings() all or TypeMemberOrdering TODO: Add tests for interfaces and records TODO: Handle inner types */ @@ -71,9 +71,20 @@ void identification() { " static class StaticInner {}", "}") .addSourceLines( - "Empty.java", - "class Empty {", + "SuppressWarningsAll.java", + "@SuppressWarnings(\"all\")", + "class SuppressWarningsAll {", + " void method() {}", + " SuppressWarningsAll() {}", "}") + .addSourceLines( + "SuppressWarningsCheck.java", + "@SuppressWarnings(\"TypeMemberOrdering\")", + "class SuppressWarningsCheck {", + " void method() {}", + " SuppressWarningsCheck() {}", + "}") + .addSourceLines("Empty.java", "class Empty {", "}") .doTest(); } From 4d772d6d7edcd9a5aa63c49aba9fde4e267c9e29 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 14:50:27 +0100 Subject: [PATCH 25/51] Suggestions --- .../bugpatterns/TypeMemberOrdering.java | 25 ++++++++++--------- .../bugpatterns/TypeMemberOrderingTest.java | 19 +++++++------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java index ce8298f389..9384419467 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java @@ -10,6 +10,7 @@ import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; +import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; @@ -64,7 +65,7 @@ public final class TypeMemberOrdering extends BugChecker implements BugChecker.C case METHOD: return isConstructor((MethodTree) tree) ? 3 : 4; default: - throw new IllegalStateException("Unexpected kind: " + tree.getKind()); + throw new VerifyException("Unexpected member kind: " + tree.getKind()); } }); @@ -72,9 +73,9 @@ public final class TypeMemberOrdering extends BugChecker implements BugChecker.C public TypeMemberOrdering() {} @Override - public Description matchClass(ClassTree classTree, VisitorState state) { + public Description matchClass(ClassTree tree, VisitorState state) { ImmutableList typeMembers = - getTypeMembersWithComments(classTree, state).stream() + getTypeMembersWithComments(tree, state).stream() .filter(typeMember -> shouldBeSorted(typeMember.tree())) .collect(toImmutableList()); @@ -86,7 +87,7 @@ public Description matchClass(ClassTree classTree, VisitorState state) { return Description.NO_MATCH; } - return buildDescription(classTree) + return buildDescription(tree) .addFix(replaceTypeMembers(typeMembers, sortedTypeMembers, state)) .build(); } @@ -112,13 +113,13 @@ private static SuggestedFix replaceTypeMembers( return Streams.zip( typeMembers.stream(), replacementTypeMembers.stream(), - (original, replacement) -> replaceTypeMember(state, original, replacement)) + (original, replacement) -> replaceTypeMember(original, replacement, state)) .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) .build(); } private static SuggestedFix replaceTypeMember( - VisitorState state, TypeMemberWithComments original, TypeMemberWithComments replacement) { + TypeMemberWithComments original, TypeMemberWithComments replacement, VisitorState state) { /* Technically this check is not necessary, but it avoids redundant replacements. */ if (original.equals(replacement)) { return SuggestedFix.emptyFix(); @@ -135,19 +136,19 @@ private static SuggestedFix replaceTypeMember( /** Returns the type's members with their comments. */ private static ImmutableList getTypeMembersWithComments( - ClassTree classTree, VisitorState state) { - return classTree.getMembers().stream() + ClassTree tree, VisitorState state) { + return tree.getMembers().stream() .map( member -> new AutoValue_TypeMemberOrdering_TypeMemberWithComments( - member, getTypeMemberComments(state, classTree, member))) + member, getTypeMemberComments(tree, member, state))) .collect(toImmutableList()); } private static ImmutableList getTypeMemberComments( - VisitorState state, ClassTree classTree, Tree member) { - int typeStart = ASTHelpers.getStartPosition(classTree); - int typeEnd = state.getEndPosition(classTree); + ClassTree tree, Tree member, VisitorState state) { + int typeStart = ASTHelpers.getStartPosition(tree); + int typeEnd = state.getEndPosition(tree); int memberStart = ASTHelpers.getStartPosition(member); int memberEnd = state.getEndPosition(member); if (typeStart == Position.NOPOS diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index f2a8012370..bfc316fcd1 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -1,6 +1,5 @@ package tech.picnic.errorprone.bugpatterns; -import static com.google.common.base.Predicates.containsPattern; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; @@ -19,7 +18,7 @@ void identification() { CompilationTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .expectErrorMessage( "TypeMemberOrdering", - containsPattern("Type members should be ordered in a standard way")) + message -> message.contains("Type members should be ordered in a standard way")) .addSourceLines( "A.java", "// BUG: Diagnostic matches: TypeMemberOrdering", @@ -75,6 +74,7 @@ void identification() { "@SuppressWarnings(\"all\")", "class SuppressWarningsAll {", " void method() {}", + "", " SuppressWarningsAll() {}", "}") .addSourceLines( @@ -82,14 +82,15 @@ void identification() { "@SuppressWarnings(\"TypeMemberOrdering\")", "class SuppressWarningsCheck {", " void method() {}", + "", " SuppressWarningsCheck() {}", "}") - .addSourceLines("Empty.java", "class Empty {", "}") + .addSourceLines("Empty.java", "class Empty {}") .doTest(); } @Test - void replacementFirstSuggestedFix() { + void replacementSuggestedFix() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", @@ -146,7 +147,7 @@ void replacementFirstSuggestedFix() { } @Test - void replacementFirstSuggestedFixConsidersDefaultConstructor() { + void replacementSuggestedFixConsidersDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", @@ -175,7 +176,7 @@ void replacementFirstSuggestedFixConsidersDefaultConstructor() { @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test - void replacementFirstSuggestedFixConsidersComments() { + void replacementSuggestedFixConsidersComments() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", @@ -215,7 +216,7 @@ void replacementFirstSuggestedFixConsidersComments() { } @Test - void replacementFirstSuggestedFixConsidersAnnotations() { + void replacementSuggestedFixConsidersAnnotations() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", @@ -240,7 +241,7 @@ void replacementFirstSuggestedFixConsidersAnnotations() { @SuppressWarnings("ErrorProneTestHelperSourceFormat") @Test - void replacementFirstSuggestedFixDoesNotModifyWhitespace() { + void replacementSuggestedFixDoesNotModifyWhitespace() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) .addInputLines( "A.java", @@ -275,7 +276,7 @@ void replacementFirstSuggestedFixDoesNotModifyWhitespace() { "", "", "}") - .doTest(); + .doTest(TestMode.TEXT_MATCH); } // XXX: This test should fail, if we verify that whitespace is preserved. From 819596bb8dea10ac237a1915fbed41d5010c19ac Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 15:11:59 +0100 Subject: [PATCH 26/51] Test initializer block ordering --- .../errorprone/bugpatterns/TypeMemberOrderingTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index bfc316fcd1..007ab444ab 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -113,6 +113,10 @@ void replacementSuggestedFix() { "", " static int TWO = 2;", "", + " { System.out.println(\"I'm an initializer block!\"); }", + "", + " static { System.out.println(\"I'm a static initializer block!\"); }", + "", " class Inner {}", "", " static class StaticInner {}", @@ -131,6 +135,10 @@ void replacementSuggestedFix() { "", " char b = 'b';", "", + " static { System.out.println(\"I'm a static initializer block!\"); }", + "", + " { System.out.println(\"I'm an initializer block!\"); }", + "", " public A() {}", "", " void m2() {}", From 9d436b948695ab0af179d5119b5d366fb58f9dca Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 15:20:42 +0100 Subject: [PATCH 27/51] Implement initializer block ordering --- .../bugpatterns/TypeMemberOrdering.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java index 9384419467..fc5ae3e802 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java @@ -21,10 +21,7 @@ import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.ErrorProneToken; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.VariableTree; +import com.sun.source.tree.*; import com.sun.source.util.TreePath; import com.sun.tools.javac.parser.Tokens; import com.sun.tools.javac.util.Position; @@ -62,8 +59,10 @@ public final class TypeMemberOrdering extends BugChecker implements BugChecker.C switch (tree.getKind()) { case VARIABLE: return isStatic((VariableTree) tree) ? 1 : 2; + case BLOCK: + return isStatic((BlockTree) tree) ? 3 : 4; case METHOD: - return isConstructor((MethodTree) tree) ? 3 : 4; + return isConstructor((MethodTree) tree) ? 5 : 6; default: throw new VerifyException("Unexpected member kind: " + tree.getKind()); } @@ -97,13 +96,18 @@ private static boolean isStatic(VariableTree variableTree) { return modifiers.contains(Modifier.STATIC); } + private static boolean isStatic(BlockTree blockTree) { + return blockTree.isStatic(); + } + private static boolean isConstructor(MethodTree methodTree) { return ASTHelpers.getSymbol(methodTree).isConstructor(); } private static boolean shouldBeSorted(Tree tree) { return tree instanceof VariableTree - || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)); + || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)) + || tree instanceof BlockTree; } private static SuggestedFix replaceTypeMembers( From b922cf0428fb632f8dc86460b581315ff558d890 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 15:39:03 +0100 Subject: [PATCH 28/51] Test inner type ordering --- .../bugpatterns/TypeMemberOrderingTest.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index 007ab444ab..4d13023fe7 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -1,6 +1,5 @@ package tech.picnic.errorprone.bugpatterns; - import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; import com.google.errorprone.CompilationTestHelper; @@ -97,6 +96,13 @@ void replacementSuggestedFix() { "class A {", " private static final int X = 1;", " char a = 'a';", + "", + " interface InnerInterface {}", + "", + " static class StaticInnerClass {}", + "", + " class InnerClass {}", + "", " private static String FOO = \"foo\";", " static int ONE = 1;", "", @@ -113,31 +119,36 @@ void replacementSuggestedFix() { "", " static int TWO = 2;", "", - " { System.out.println(\"I'm an initializer block!\"); }", - "", - " static { System.out.println(\"I'm a static initializer block!\"); }", - "", - " class Inner {}", + " {", + " System.out.println(\"I'm an initializer block!\");", + " }", "", - " static class StaticInner {}", + " static {", + " System.out.println(\"I'm a static initializer block!\");", + " }", "}") .addOutputLines( "A.java", "class A {", " private static final int X = 1;", " private static String FOO = \"foo\";", + "", " static int ONE = 1;", + "", " private static String BAR = \"bar\";", "", " static int TWO = 2;", "", " char a = 'a';", - "", " char b = 'b';", "", - " static { System.out.println(\"I'm a static initializer block!\"); }", + " static {", + " System.out.println(\"I'm a static initializer block!\");", + " }", "", - " { System.out.println(\"I'm an initializer block!\"); }", + " {", + " System.out.println(\"I'm an initializer block!\");", + " }", "", " public A() {}", "", @@ -147,9 +158,11 @@ void replacementSuggestedFix() { " System.out.println(\"foo\");", " }", "", - " class Inner {}", + " interface InnerInterface {}", "", - " static class StaticInner {}", + " class InnerClass {}", + "", + " static class StaticInnerClass {}", "}") .doTest(TestMode.TEXT_MATCH); } From ed5efe36e3186af349e3d68306c1801a05134ee9 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 15:39:17 +0100 Subject: [PATCH 29/51] Implement inner type ordering --- .../bugpatterns/TypeMemberOrdering.java | 17 +++++++++++++++-- .../bugpatterns/TypeMemberOrderingTest.java | 2 -- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java index fc5ae3e802..a16484d63b 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java @@ -21,7 +21,11 @@ import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.google.errorprone.util.ErrorProneToken; -import com.sun.source.tree.*; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.parser.Tokens; import com.sun.tools.javac.util.Position; @@ -63,6 +67,9 @@ public final class TypeMemberOrdering extends BugChecker implements BugChecker.C return isStatic((BlockTree) tree) ? 3 : 4; case METHOD: return isConstructor((MethodTree) tree) ? 5 : 6; + case CLASS: + case INTERFACE: + return isStatic((ClassTree) tree) ? 8 : 7; default: throw new VerifyException("Unexpected member kind: " + tree.getKind()); } @@ -100,6 +107,11 @@ private static boolean isStatic(BlockTree blockTree) { return blockTree.isStatic(); } + private static boolean isStatic(ClassTree classTree) { + Set modifiers = classTree.getModifiers().getFlags(); + return modifiers.contains(Modifier.STATIC); + } + private static boolean isConstructor(MethodTree methodTree) { return ASTHelpers.getSymbol(methodTree).isConstructor(); } @@ -107,7 +119,8 @@ private static boolean isConstructor(MethodTree methodTree) { private static boolean shouldBeSorted(Tree tree) { return tree instanceof VariableTree || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)) - || tree instanceof BlockTree; + || tree instanceof BlockTree + || tree instanceof ClassTree; } private static SuggestedFix replaceTypeMembers( diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index 4d13023fe7..efa358ff7c 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -6,10 +6,8 @@ import org.junit.jupiter.api.Test; /* -TODO: Order static and non-static initializer blocks. TODO: Skip members annotated with @SuppressWarnings() all or TypeMemberOrdering TODO: Add tests for interfaces and records -TODO: Handle inner types */ final class TypeMemberOrderingTest { @Test From f63971e31028456f33df80a9eee3f729b9d963cd Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 15:50:57 +0100 Subject: [PATCH 30/51] Test not-ordering unordered members annotated w/ SuppressWarnings --- .../bugpatterns/TypeMemberOrderingTest.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java index efa358ff7c..11ff2b0062 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; /* -TODO: Skip members annotated with @SuppressWarnings() all or TypeMemberOrdering TODO: Add tests for interfaces and records */ final class TypeMemberOrderingTest { @@ -67,20 +66,20 @@ void identification() { " static class StaticInner {}", "}") .addSourceLines( - "SuppressWarningsAll.java", - "@SuppressWarnings(\"all\")", - "class SuppressWarningsAll {", - " void method() {}", + "C.java", + "class C {", + " @SuppressWarnings({\"foo\", \"all\", \"bar\"})", + " void unorderedMethod() {}", "", - " SuppressWarningsAll() {}", + " C() {}", "}") .addSourceLines( - "SuppressWarningsCheck.java", - "@SuppressWarnings(\"TypeMemberOrdering\")", - "class SuppressWarningsCheck {", - " void method() {}", + "D.java", + "class D {", + " @SuppressWarnings(\"TypeMemberOrdering\")", + " void unorderedMethod() {}", "", - " SuppressWarningsCheck() {}", + " D() {}", "}") .addSourceLines("Empty.java", "class Empty {}") .doTest(); From d93e8a172630d2875048f890871ad739d77d26aa Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 16:54:58 +0100 Subject: [PATCH 31/51] Stop sorting unsorted members annotated w/ SuppressWarnings - all or TypeMemberOrdering --- .../bugpatterns/TypeMemberOrdering.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java index a16484d63b..95911c1a71 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java @@ -12,6 +12,7 @@ import com.google.auto.value.AutoValue; import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; @@ -27,6 +28,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; +import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.parser.Tokens; import com.sun.tools.javac.util.Position; import java.util.Comparator; @@ -75,6 +77,13 @@ public final class TypeMemberOrdering extends BugChecker implements BugChecker.C } }); + /** + * Collection of values that are when provided in a {@link SuppressWarnings} annotation of a + * member, this BugChecker will not sort it. + */ + private static final ImmutableSet RECOGNIZED_SUPPRESSIONS = + ImmutableSet.of("all", TypeMemberOrdering.class.getSimpleName()); + /** Instantiates a new {@link TypeMemberOrdering} instance. */ public TypeMemberOrdering() {} @@ -117,12 +126,36 @@ private static boolean isConstructor(MethodTree methodTree) { } private static boolean shouldBeSorted(Tree tree) { + if (hasRecognizedSuppressWarnings(tree)) { + return false; + } + ; return tree instanceof VariableTree || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)) || tree instanceof BlockTree || tree instanceof ClassTree; } + private static Boolean hasRecognizedSuppressWarnings(Tree tree) { + return Optional.ofNullable( + ASTHelpers.getAnnotationWithSimpleName( + ASTHelpers.getAnnotations(tree), "SuppressWarnings")) + .flatMap( + suppressWarningsTree -> + ASTHelpers.getAnnotationMirror(suppressWarningsTree) + .getElementValues() + .values() + .stream() + // Assuming SuppressWarnings has a single member (`String[] value()`) + .findAny()) + .map( + annotationValue -> + ((Attribute.Array) annotationValue) + .getValue().map(attr -> (String) attr.getValue()).stream() + .anyMatch(RECOGNIZED_SUPPRESSIONS::contains)) + .orElse(false); + } + private static SuggestedFix replaceTypeMembers( ImmutableList typeMembers, ImmutableList replacementTypeMembers, From 738dcf24623f03e44cef1aa546bd0657f96236a3 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 14 Jan 2024 17:16:06 +0100 Subject: [PATCH 32/51] Rename `TypeMemberWithComments` to `TypeMember` --- .../bugpatterns/TypeMemberOrdering.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java index 95911c1a71..f3975930ca 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java @@ -89,14 +89,14 @@ public TypeMemberOrdering() {} @Override public Description matchClass(ClassTree tree, VisitorState state) { - ImmutableList typeMembers = + ImmutableList typeMembers = getTypeMembersWithComments(tree, state).stream() .filter(typeMember -> shouldBeSorted(typeMember.tree())) .collect(toImmutableList()); - ImmutableList sortedTypeMembers = + ImmutableList sortedTypeMembers = ImmutableList.sortedCopyOf( - comparing(TypeMemberWithComments::tree, BY_PREFERRED_TYPE_MEMBER_ORDER), typeMembers); + comparing(TypeMember::tree, BY_PREFERRED_TYPE_MEMBER_ORDER), typeMembers); if (typeMembers.equals(sortedTypeMembers)) { return Description.NO_MATCH; @@ -157,8 +157,8 @@ private static Boolean hasRecognizedSuppressWarnings(Tree tree) { } private static SuggestedFix replaceTypeMembers( - ImmutableList typeMembers, - ImmutableList replacementTypeMembers, + ImmutableList typeMembers, + ImmutableList replacementTypeMembers, VisitorState state) { return Streams.zip( typeMembers.stream(), @@ -169,7 +169,7 @@ private static SuggestedFix replaceTypeMembers( } private static SuggestedFix replaceTypeMember( - TypeMemberWithComments original, TypeMemberWithComments replacement, VisitorState state) { + TypeMember original, TypeMember replacement, VisitorState state) { /* Technically this check is not necessary, but it avoids redundant replacements. */ if (original.equals(replacement)) { return SuggestedFix.emptyFix(); @@ -185,12 +185,12 @@ private static SuggestedFix replaceTypeMember( } /** Returns the type's members with their comments. */ - private static ImmutableList getTypeMembersWithComments( + private static ImmutableList getTypeMembersWithComments( ClassTree tree, VisitorState state) { return tree.getMembers().stream() .map( member -> - new AutoValue_TypeMemberOrdering_TypeMemberWithComments( + new AutoValue_TypeMemberOrdering_TypeMember( member, getTypeMemberComments(tree, member, state))) .collect(toImmutableList()); } @@ -228,7 +228,7 @@ private static ImmutableList getTypeMemberComments( } @AutoValue - abstract static class TypeMemberWithComments { + abstract static class TypeMember { abstract Tree tree(); abstract ImmutableList comments(); From e5d1aa2a67e773c6f39092b61898cf3690791076 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 21 Jan 2024 10:25:59 +0100 Subject: [PATCH 33/51] Support classes, interfaces and enums --- .../bugpatterns/TypeMemberOrder.java | 274 ++++++++++++++ .../bugpatterns/TypeMemberOrdering.java | 236 ------------ .../bugpatterns/TypeMemberOrderClassTest.java | 288 +++++++++++++++ .../bugpatterns/TypeMemberOrderEnumTest.java | 310 ++++++++++++++++ .../TypeMemberOrderInterfaceTest.java | 165 +++++++++ .../bugpatterns/TypeMemberOrderingTest.java | 345 ------------------ 6 files changed, 1037 insertions(+), 581 deletions(-) create mode 100644 error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java delete mode 100644 error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java create mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java create mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java create mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java delete mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java new file mode 100644 index 0000000000..e7035f5f75 --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -0,0 +1,274 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.common.base.Verify.verify; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.errorprone.BugPattern.LinkType.CUSTOM; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.BugPattern.StandardTags.STYLE; +import static com.sun.tools.javac.code.Flags.ENUM; +import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; +import static java.util.Objects.requireNonNull; +import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; + +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import com.google.common.collect.Comparators; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Streams; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.annotations.Var; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.util.ASTHelpers; +import com.google.errorprone.util.ErrorProneToken; +import com.google.errorprone.util.ErrorProneTokens; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.parser.Tokens.TokenKind; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.Position; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import javax.lang.model.element.Modifier; + +/** + * A {@link BugChecker} that flags classes with a non-canonical member order. + * + *

Class members should be ordered as follows: + * + *

    + *
  1. Static fields + *
  2. Instance fields + *
  3. Static initializer blocks + *
  4. Instance initializer blocks + *
  5. Constructors + *
  6. Methods + *
  7. Nested classes, interfaces and enums + *
+ * + * @see Checkstyle's + * {@code DeclarationOrderCheck} + */ +// XXX: Reference +// https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html +@AutoService(BugChecker.class) +@BugPattern( + summary = "Type members should be ordered in a standard way", + link = BUG_PATTERNS_BASE_URL + "TypeMemberOrder", + linkType = CUSTOM, + severity = WARNING, + tags = STYLE) +public final class TypeMemberOrder extends BugChecker implements BugChecker.ClassTreeMatcher { + private static final long serialVersionUID = 1L; + + /** Instantiates a new {@link TypeMemberOrder} instance. */ + public TypeMemberOrder() {} + + @Override + public Description matchClass(ClassTree tree, VisitorState state) { + if (tree.getKind() != Tree.Kind.CLASS + && tree.getKind() != Tree.Kind.INTERFACE + && tree.getKind() != Tree.Kind.ENUM) { + return Description.NO_MATCH; + } + + // Keep track of all members, including unmovable ones, to exclude them from the sources + // present in-between members. + ImmutableList members = + tree.getMembers().stream() + .filter(TypeMemberOrder::hasSource) + .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) + .collect(toImmutableList()); + + // List of the sortable members' preferred ordinals, + // ordered by the member's position in original source. + ImmutableList preferredOrdinals = + members.stream() + .filter(m -> m.preferredOrdinal().isPresent()) + .map(m -> m.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ )) + .collect(toImmutableList()); + + if (Comparators.isInOrder(preferredOrdinals, naturalOrder())) { + return Description.NO_MATCH; + } + + int bodyStartPos = getBodyStartPos(tree, state); + if (bodyStartPos == Position.NOPOS) { + /* + * We can't determine the type body's start position in the source code. This generally means + * that (part of) its code was generated. Even if the source code for a subset of its members + * is available, dealing with this edge case is not worth the trouble. + */ + return Description.NO_MATCH; + } + + return describeMatch(tree, sortTypeMembers(bodyStartPos, members, state)); + } + + /** + * Returns the preferred ordinal of the given member, or empty if it's unmovable for any reason, + * including it lacking a preferred ordinal. + */ + private Optional getPreferredOrdinal(Tree tree, VisitorState state) { + if (!canMove(tree, state)) { + return Optional.empty(); + } + switch (tree.getKind()) { + case VARIABLE: + return Optional.of(isStatic((VariableTree) tree) ? 1 : 2); + case BLOCK: + return Optional.of(isStatic((BlockTree) tree) ? 3 : 4); + case METHOD: + return Optional.of(isConstructor((MethodTree) tree) ? 5 : 6); + case CLASS: + case INTERFACE: + case ENUM: + return Optional.of(7); + default: + // TODO: Should we log unhandled kinds? + return Optional.empty(); + } + } + + private boolean canMove(Tree tree, VisitorState state) { + return hasSource(tree) && !isSuppressed(tree, state) && !isEnumerator(tree); + } + + private static boolean hasSource(Tree tree) { + if (tree.getKind() == Tree.Kind.METHOD) { + return !ASTHelpers.isGeneratedConstructor(((MethodTree) tree)); + } + return true; + } + + /** + * Returns true if Tree is an enumerator of an enumerated type, or false otherwise. + * + * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) + * @see com.sun.tools.javac.code.Flags#ENUM + */ + private static boolean isEnumerator(Tree tree) { + return tree instanceof JCTree.JCVariableDecl + && (((JCTree.JCVariableDecl) tree).mods.flags & ENUM) != 0; + } + + /** + * Returns the start position of the body of the given type, in the case of enums, it returns the + * position that follows the enumerated type's enumerations. + */ + private static int getBodyStartPos(ClassTree tree, VisitorState state) { + CharSequence sourceCode = state.getSourceCode(); + int typeStart = ASTHelpers.getStartPosition(tree); + int typeEnd = state.getEndPosition(tree); + if (sourceCode == null || typeStart == Position.NOPOS || typeEnd == Position.NOPOS) { + return Position.NOPOS; + } + + // We return the source code position of the first token that follows the first left brace. + return ErrorProneTokens.getTokens( + sourceCode.subSequence(typeStart, typeEnd).toString(), typeStart, state.context) + .stream() + .dropWhile(token -> token.kind() != TokenKind.LBRACE) + // XXX: To accommodate enums, start processing the body, after the enumerated type's + // enumerators. + .dropWhile(token -> tree.getKind() == Tree.Kind.ENUM && token.kind() != TokenKind.SEMI) + .findFirst() + .map(ErrorProneToken::endPos) + .orElse(Position.NOPOS); + } + + /** + * Suggests a different way of ordering the given type members. + * + * @implNote For each member, this method tracks the source code between the end of the definition + * of the member that precedes it (or the start of the type body if there is no such member) + * and the end of the definition of the member itself. This subsequently enables moving + * members around, including any preceding comments and Javadoc. This approach isn't perfect, + * and may at times move too much code or documentation around; users will have to manually + * resolve this. + */ + private static SuggestedFix sortTypeMembers( + int bodyStartPos, ImmutableList members, VisitorState state) { + List membersWithSource = new ArrayList<>(); + + @Var int start = bodyStartPos; + for (TypeMember member : members) { + int end = state.getEndPosition(member.tree()); + if (isEnumerator(member.tree())) { + // XXX: To accommodate enums, skip enumerators of enumerated types. + continue; + } + verify( + end != Position.NOPOS && start < end, + "Unexpected member end position, member: %s", + member); + if (member.preferredOrdinal().isPresent()) { + membersWithSource.add( + new AutoValue_TypeMemberOrder_MovableTypeMember( + member.tree(), + start, + end, + member.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ ))); + } + start = end; + } + + CharSequence sourceCode = requireNonNull(state.getSourceCode(), "Source code"); + return Streams.zip( + membersWithSource.stream(), + membersWithSource.stream() + .sorted(comparing(MovableTypeMember::preferredOrdinal, naturalOrder())), + (original, replacement) -> original.replaceWith(replacement, sourceCode)) + .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) + .build(); + } + + private static boolean isStatic(VariableTree variableTree) { + Set modifiers = variableTree.getModifiers().getFlags(); + return modifiers.contains(Modifier.STATIC); + } + + private static boolean isStatic(BlockTree blockTree) { + return blockTree.isStatic(); + } + + private static boolean isConstructor(MethodTree methodTree) { + return ASTHelpers.getSymbol(methodTree).isConstructor(); + } + + @AutoValue + abstract static class TypeMember { + abstract Tree tree(); + + abstract Optional preferredOrdinal(); + } + + @AutoValue + abstract static class MovableTypeMember { + abstract Tree tree(); + + abstract int startPosition(); + + abstract int endPosition(); + + abstract int preferredOrdinal(); + + SuggestedFix replaceWith(MovableTypeMember other, CharSequence fullSourceCode) { + return equals(other) + ? SuggestedFix.emptyFix() + : SuggestedFix.replace( + startPosition(), + endPosition(), + fullSourceCode.subSequence(other.startPosition(), other.endPosition()).toString()); + } + } +} diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java deleted file mode 100644 index f3975930ca..0000000000 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrdering.java +++ /dev/null @@ -1,236 +0,0 @@ -package tech.picnic.errorprone.bugpatterns; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.errorprone.BugPattern.LinkType.CUSTOM; -import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; -import static com.google.errorprone.BugPattern.StandardTags.STYLE; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.joining; -import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; - -import com.google.auto.service.AutoService; -import com.google.auto.value.AutoValue; -import com.google.common.base.VerifyException; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Streams; -import com.google.errorprone.BugPattern; -import com.google.errorprone.VisitorState; -import com.google.errorprone.bugpatterns.BugChecker; -import com.google.errorprone.fixes.SuggestedFix; -import com.google.errorprone.fixes.SuggestedFixes; -import com.google.errorprone.matchers.Description; -import com.google.errorprone.util.ASTHelpers; -import com.google.errorprone.util.ErrorProneToken; -import com.sun.source.tree.BlockTree; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.VariableTree; -import com.sun.source.util.TreePath; -import com.sun.tools.javac.code.Attribute; -import com.sun.tools.javac.parser.Tokens; -import com.sun.tools.javac.util.Position; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; -import javax.lang.model.element.Modifier; -import tech.picnic.errorprone.bugpatterns.util.SourceCode; - -/** - * A {@link BugChecker} that flags types with non-standard member ordering. - * - *

Type members should be ordered in a standard way, which is: static fields, non-static fields, - * constructors and methods. - */ -// XXX: Reference -// https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html -@AutoService(BugChecker.class) -@BugPattern( - summary = "Type members should be ordered in a standard way", - link = BUG_PATTERNS_BASE_URL + "TypeMemberOrdering", - linkType = CUSTOM, - severity = WARNING, - tags = STYLE) -public final class TypeMemberOrdering extends BugChecker implements BugChecker.ClassTreeMatcher { - private static final long serialVersionUID = 1L; - - // TODO: Copy should be sorted and comparator in-sync. - /** Orders {@link Tree}s to match the standard Java type member declaration order. */ - private static final Comparator BY_PREFERRED_TYPE_MEMBER_ORDER = - comparing( - tree -> { - switch (tree.getKind()) { - case VARIABLE: - return isStatic((VariableTree) tree) ? 1 : 2; - case BLOCK: - return isStatic((BlockTree) tree) ? 3 : 4; - case METHOD: - return isConstructor((MethodTree) tree) ? 5 : 6; - case CLASS: - case INTERFACE: - return isStatic((ClassTree) tree) ? 8 : 7; - default: - throw new VerifyException("Unexpected member kind: " + tree.getKind()); - } - }); - - /** - * Collection of values that are when provided in a {@link SuppressWarnings} annotation of a - * member, this BugChecker will not sort it. - */ - private static final ImmutableSet RECOGNIZED_SUPPRESSIONS = - ImmutableSet.of("all", TypeMemberOrdering.class.getSimpleName()); - - /** Instantiates a new {@link TypeMemberOrdering} instance. */ - public TypeMemberOrdering() {} - - @Override - public Description matchClass(ClassTree tree, VisitorState state) { - ImmutableList typeMembers = - getTypeMembersWithComments(tree, state).stream() - .filter(typeMember -> shouldBeSorted(typeMember.tree())) - .collect(toImmutableList()); - - ImmutableList sortedTypeMembers = - ImmutableList.sortedCopyOf( - comparing(TypeMember::tree, BY_PREFERRED_TYPE_MEMBER_ORDER), typeMembers); - - if (typeMembers.equals(sortedTypeMembers)) { - return Description.NO_MATCH; - } - - return buildDescription(tree) - .addFix(replaceTypeMembers(typeMembers, sortedTypeMembers, state)) - .build(); - } - - private static boolean isStatic(VariableTree variableTree) { - Set modifiers = variableTree.getModifiers().getFlags(); - return modifiers.contains(Modifier.STATIC); - } - - private static boolean isStatic(BlockTree blockTree) { - return blockTree.isStatic(); - } - - private static boolean isStatic(ClassTree classTree) { - Set modifiers = classTree.getModifiers().getFlags(); - return modifiers.contains(Modifier.STATIC); - } - - private static boolean isConstructor(MethodTree methodTree) { - return ASTHelpers.getSymbol(methodTree).isConstructor(); - } - - private static boolean shouldBeSorted(Tree tree) { - if (hasRecognizedSuppressWarnings(tree)) { - return false; - } - ; - return tree instanceof VariableTree - || (tree instanceof MethodTree && !ASTHelpers.isGeneratedConstructor((MethodTree) tree)) - || tree instanceof BlockTree - || tree instanceof ClassTree; - } - - private static Boolean hasRecognizedSuppressWarnings(Tree tree) { - return Optional.ofNullable( - ASTHelpers.getAnnotationWithSimpleName( - ASTHelpers.getAnnotations(tree), "SuppressWarnings")) - .flatMap( - suppressWarningsTree -> - ASTHelpers.getAnnotationMirror(suppressWarningsTree) - .getElementValues() - .values() - .stream() - // Assuming SuppressWarnings has a single member (`String[] value()`) - .findAny()) - .map( - annotationValue -> - ((Attribute.Array) annotationValue) - .getValue().map(attr -> (String) attr.getValue()).stream() - .anyMatch(RECOGNIZED_SUPPRESSIONS::contains)) - .orElse(false); - } - - private static SuggestedFix replaceTypeMembers( - ImmutableList typeMembers, - ImmutableList replacementTypeMembers, - VisitorState state) { - return Streams.zip( - typeMembers.stream(), - replacementTypeMembers.stream(), - (original, replacement) -> replaceTypeMember(original, replacement, state)) - .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) - .build(); - } - - private static SuggestedFix replaceTypeMember( - TypeMember original, TypeMember replacement, VisitorState state) { - /* Technically this check is not necessary, but it avoids redundant replacements. */ - if (original.equals(replacement)) { - return SuggestedFix.emptyFix(); - } - - String replacementSource = - Stream.concat( - replacement.comments().stream(), - Stream.of(SourceCode.treeToString(replacement.tree(), state))) - .collect(joining(System.lineSeparator())); - return SuggestedFixes.replaceIncludingComments( - TreePath.getPath(state.getPath(), original.tree()), replacementSource, state); - } - - /** Returns the type's members with their comments. */ - private static ImmutableList getTypeMembersWithComments( - ClassTree tree, VisitorState state) { - return tree.getMembers().stream() - .map( - member -> - new AutoValue_TypeMemberOrdering_TypeMember( - member, getTypeMemberComments(tree, member, state))) - .collect(toImmutableList()); - } - - private static ImmutableList getTypeMemberComments( - ClassTree tree, Tree member, VisitorState state) { - int typeStart = ASTHelpers.getStartPosition(tree); - int typeEnd = state.getEndPosition(tree); - int memberStart = ASTHelpers.getStartPosition(member); - int memberEnd = state.getEndPosition(member); - if (typeStart == Position.NOPOS - || typeEnd == Position.NOPOS - || memberStart == Position.NOPOS - || memberEnd == Position.NOPOS) { - /* Source code details appear to be unavailable. */ - return ImmutableList.of(); - } - - // TODO: Move identifying "previous member end position" to an outer loop, - // Loop once and identify for all members - // TODO: Check if this handles properly comments on the first member. - Optional previousMemberEndPos = - state.getOffsetTokens(typeStart, typeEnd).stream() - .map(ErrorProneToken::endPos) - .takeWhile(endPos -> endPos < memberStart) - .reduce((earlierPos, laterPos) -> laterPos); - - List typeMemberTokens = - state.getOffsetTokens(previousMemberEndPos.orElse(memberStart), memberEnd); - - // TODO: double check this .get(0) - return typeMemberTokens.get(0).comments().stream() - .map(Tokens.Comment::getText) - .collect(toImmutableList()); - } - - @AutoValue - abstract static class TypeMember { - abstract Tree tree(); - - abstract ImmutableList comments(); - } -} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java new file mode 100644 index 0000000000..1abd7a7190 --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java @@ -0,0 +1,288 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +final class TypeMemberOrderClassTest { + @Test + void identification() { + CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .expectErrorMessage( + "TypeMemberOrder", + message -> message.contains("Type members should be ordered in a standard way")) + .addSourceLines( + "A.java", + "// BUG: Diagnostic matches: TypeMemberOrder", + "class A {", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "", + " int foo() {", + " return foo;", + " }", + "", + " A() {}", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " private final int bar = 2;", + " private static final int foo = 1;", + "}") + .addSourceLines( + "B.java", + "class B {", + " private static final int foo = 1;", + " private final int bar = 2;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " B() {}", + "", + " int foo() {", + " return foo;", + " }", + "", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "}") + .addSourceLines( + "C.java", + "class C {", + " @SuppressWarnings({\"foo\", \"all\", \"bar\"})", + " void unorderedMethod() {}", + "", + " C() {}", + "}") + .addSourceLines( + "D.java", + "class D {", + " @SuppressWarnings(\"TypeMemberOrder\")", + " void unorderedMethod() {}", + "", + " D() {}", + "}") + .addSourceLines("E.java", "class E {}") + .doTest(); + } + + @Test + void replacementSuggestedFix() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "class A {", + " class Inner {}", + "", + " int foo() {", + " return foo;", + " }", + "", + " A() {}", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " private final int bar = 2;", + " private static final int foo = 1;", + "}") + .addOutputLines( + "A.java", + "class A {", + " private static final int foo = 1;", + "", + " private final int bar = 2;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " A() {}", + "", + " int foo() {", + " return foo;", + " }", + "", + " class Inner {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersUnmovableMembers() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "class A {", + " @SuppressWarnings(\"TypeMemberOrder\")", + " int foo() {", + " return foo;", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " private final int bar = 2;", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " private static final int foo = 1;", + "}") + .addOutputLines( + "A.java", + "class A {", + " @SuppressWarnings(\"TypeMemberOrder\")", + " int foo() {", + " return foo;", + " }", + "", + " private final int bar = 2;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " private static final int foo = 1;", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersDefaultConstructor() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "class A {", + " int foo() {", + " return foo;", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " private final int bar = 2;", + " private static final int foo = 1;", + "}") + .addOutputLines( + "A.java", + "class A {", + " private static final int foo = 1;", + "", + " private final int bar = 2;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " int foo() {", + " return foo;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersDanglingComments() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "class A {", + " /* `foo` method's dangling comment. */", + " ;", + " // `foo` method's comment", + " int foo() {", + " return foo;", + " }", + "", + " // initializer block's comment", + " {", + " System.out.println(\"bar\");", + " }", + "", + " // static initializer block's comment", + " static {", + " System.out.println(\"foo\");", + " }", + " /* `bar` field's dangling comment. */ ;", + "", + " private final int bar = 2;", + " // `foo` field's comment", + " private static final int foo = 1;", + " // trailing comment", + "}") + .addOutputLines( + "A.java", + "class A {", + " // `foo` field's comment", + " private static final int foo = 1;", + " /* `bar` field's dangling comment. */ ;", + "", + " private final int bar = 2;", + "", + " // static initializer block's comment", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " // initializer block's comment", + " {", + " System.out.println(\"bar\");", + " }", + " /* `foo` method's dangling comment. */", + " ;", + "", + " // `foo` method's comment", + " int foo() {", + " return foo;", + " }", + " // trailing comment", + "}") + .doTest(TestMode.TEXT_MATCH); + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java new file mode 100644 index 0000000000..b50e4f680e --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java @@ -0,0 +1,310 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +final class TypeMemberOrderEnumTest { + @Test + void identification() { + CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .expectErrorMessage( + "TypeMemberOrder", + message -> message.contains("Type members should be ordered in a standard way")) + .addSourceLines( + "A.java", + "// BUG: Diagnostic matches: TypeMemberOrder", + "enum A {", + " FOO;", + "", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "", + " void qux() {}", + "", + " A() {}", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " final int baz = 2;", + " static final int BAR = 1;", + "}") + .addSourceLines( + "B.java", + "enum B {", + " FOO;", + " static final int BAR = 1;", + " final int baz = 2;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " B() {}", + "", + " void qux() {}", + "", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "}") + .addSourceLines( + "C.java", + "enum C {", + " FOO;", + "", + " @SuppressWarnings({\"foo\", \"all\", \"bar\"})", + " void bar() {}", + "", + " C() {}", + "}") + .addSourceLines( + "D.java", + "enum D {", + " FOO;", + "", + " @SuppressWarnings({\"TypeMemberOrder\"})", + " void bar() {}", + "", + " D() {}", + "}") + .addSourceLines("E.java", "enum E {}") + .doTest(); + } + + @Test + void replacementSuggestedFix() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "enum A {", + " FOO;", + "", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "", + " void qux() {}", + "", + " A() {}", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " final int baz = 2;", + " static final int BAR = 1;", + "}") + .addOutputLines( + "A.java", + "enum A {", + " FOO;", + " static final int BAR = 1;", + "", + " final int baz = 2;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " A() {}", + "", + " void qux() {}", + "", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersUnmovableMembers() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "enum A {", + " FOO;", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "", + " void qux() {}", + "", + " A() {}", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " final int baz = 2;", + "", + " static final int BAR = 1;", + "}") + .addOutputLines( + "A.java", + "enum A {", + " FOO;", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " class InnerClass {}", + "", + " static final int BAR = 1;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " A() {}", + "", + " void qux() {}", + "", + " interface InnerInterface {}", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " final int baz = 2;", + "", + " enum InnerEnum {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersDefaultConstructor() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "enum A {", + " FOO;", + "", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "", + " void qux() {}", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " final int baz = 2;", + " static final int BAR = 1;", + "}") + .addOutputLines( + "A.java", + "enum A {", + " FOO;", + " static final int BAR = 1;", + "", + " final int baz = 2;", + "", + " static {", + " System.out.println(\"foo\");", + " }", + "", + " {", + " System.out.println(\"bar\");", + " }", + "", + " void qux() {}", + "", + " class InnerClass {}", + "", + " interface InnerInterface {}", + "", + " enum InnerEnum {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersDanglingComments() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "enum A {", + " /** FOO's JavaDoc */", + " FOO", + "/* Dangling comment trailing enumerations. */ ;", + "", + " /* `quz` method's dangling comment */", + " ;", + "", + " /* `quz` method's comment */", + " void qux() {}", + "", + " // `baz` method's comment", + " final int baz = 2;", + "", + " static final int BAR = 1;", + " // trailing comment", + "}") + .addOutputLines( + "A.java", + "enum A {", + " /** FOO's JavaDoc */", + " FOO", + "/* Dangling comment trailing enumerations. */ ;", + "", + " static final int BAR = 1;", + "", + " // `baz` method's comment", + " final int baz = 2;", + "", + " /* `quz` method's dangling comment */", + " ;", + "", + " /* `quz` method's comment */", + " void qux() {}", + " // trailing comment", + "}") + .doTest(TestMode.TEXT_MATCH); + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java new file mode 100644 index 0000000000..9647d1fb57 --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java @@ -0,0 +1,165 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +final class TypeMemberOrderInterfaceTest { + @Test + void identification() { + CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .expectErrorMessage( + "TypeMemberOrder", + message -> message.contains("Type members should be ordered in a standard way")) + .addSourceLines( + "A.java", + "// BUG: Diagnostic matches: TypeMemberOrder", + "interface A {", + " static class InnerClass {}", + "", + " static interface InnerInterface {}", + "", + " static enum InnerEnum {}", + "", + " void bar();", + "", + " static final int foo = 1;", + "}") + .addSourceLines( + "B.java", + "interface B {", + " static final int foo = 1;", + "", + " void bar();", + "", + " static class InnerClass {}", + "", + " static interface InnerInterface {}", + "", + " static enum InnerEnum {}", + "}") + .addSourceLines( + "C.java", + "interface C {", + " @SuppressWarnings({\"foo\", \"all\", \"bar\"})", + " void bar();", + "", + " static final int foo = 1;", + "}") + .addSourceLines( + "D.java", + "interface D {", + " @SuppressWarnings(\"TypeMemberOrder\")", + " void bar();", + "", + " static final int foo = 1;", + "}") + .addSourceLines("E.java", "interface E {}") + .doTest(); + } + + @Test + void replacementSuggestedFix() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "interface A {", + " static class InnerClass {}", + "", + " static interface InnerInterface {}", + "", + " static enum InnerEnum {}", + "", + " void bar();", + "", + " static final int foo = 1;", + "}") + .addOutputLines( + "A.java", + "interface A {", + "", + " static final int foo = 1;", + "", + " void bar();", + "", + " static class InnerClass {}", + "", + " static interface InnerInterface {}", + "", + " static enum InnerEnum {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersUnmovableMembers() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "interface A {", + " @SuppressWarnings(\"TypeMemberOrder\")", + " static class InnerClass {}", + "", + " static interface InnerInterface {}", + "", + " static enum InnerEnum {}", + "", + " void bar();", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " static final int baz = 1;", + "", + " static final int foo = 1;", + "}") + .addOutputLines( + "A.java", + "interface A {", + " @SuppressWarnings(\"TypeMemberOrder\")", + " static class InnerClass {}", + "", + " static final int foo = 1;", + "", + " void bar();", + "", + " static interface InnerInterface {}", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " static final int baz = 1;", + "", + " static enum InnerEnum {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSuggestedFixConsidersDanglingComments() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "interface A {", + " // `bar` method's dangling comment.", + " ;", + " // `bar` method's comment", + " void bar();", + "", + " // `foo` method's comment", + " static final int foo = 1;", + " // trailing comment", + "}") + .addOutputLines( + "A.java", + "interface A {", + "", + " // `foo` method's comment", + " static final int foo = 1;", + " // `bar` method's dangling comment.", + " ;", + "", + " // `bar` method's comment", + " void bar();", + " // trailing comment", + "}") + .doTest(TestMode.TEXT_MATCH); + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java deleted file mode 100644 index 11ff2b0062..0000000000 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderingTest.java +++ /dev/null @@ -1,345 +0,0 @@ -package tech.picnic.errorprone.bugpatterns; - -import com.google.errorprone.BugCheckerRefactoringTestHelper; -import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; -import com.google.errorprone.CompilationTestHelper; -import org.junit.jupiter.api.Test; - -/* -TODO: Add tests for interfaces and records -*/ -final class TypeMemberOrderingTest { - @Test - void identification() { - CompilationTestHelper.newInstance(TypeMemberOrdering.class, getClass()) - .expectErrorMessage( - "TypeMemberOrdering", - message -> message.contains("Type members should be ordered in a standard way")) - .addSourceLines( - "A.java", - "// BUG: Diagnostic matches: TypeMemberOrdering", - "class A {", - " char a = 'a';", - " private static String FOO = \"foo\";", - " static int ONE = 1;", - "", - " void m2() {}", - "", - " public A() {}", - "", - " private static String BAR = \"bar\";", - " char b = 'b';", - "", - " void m1() {", - " System.out.println(\"foo\");", - " }", - "", - " static int TWO = 2;", - "", - " class Inner {}", - "", - " static class StaticInner {}", - "}") - .addSourceLines( - "B.java", - "class B {", - " private static String FOO = \"foo\";", - " static int ONE = 1;", - " private static String BAR = \"bar\";", - "", - " static int TWO = 2;", - "", - " char a = 'a';", - "", - " char b = 'b';", - "", - " public B() {}", - "", - " void m1() {", - " System.out.println(\"foo\");", - " }", - "", - " void m2() {}", - "", - " class Inner {}", - "", - " static class StaticInner {}", - "}") - .addSourceLines( - "C.java", - "class C {", - " @SuppressWarnings({\"foo\", \"all\", \"bar\"})", - " void unorderedMethod() {}", - "", - " C() {}", - "}") - .addSourceLines( - "D.java", - "class D {", - " @SuppressWarnings(\"TypeMemberOrdering\")", - " void unorderedMethod() {}", - "", - " D() {}", - "}") - .addSourceLines("Empty.java", "class Empty {}") - .doTest(); - } - - @Test - void replacementSuggestedFix() { - BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) - .addInputLines( - "A.java", - "class A {", - " private static final int X = 1;", - " char a = 'a';", - "", - " interface InnerInterface {}", - "", - " static class StaticInnerClass {}", - "", - " class InnerClass {}", - "", - " private static String FOO = \"foo\";", - " static int ONE = 1;", - "", - " void m2() {}", - "", - " public A() {}", - "", - " private static String BAR = \"bar\";", - " char b = 'b';", - "", - " void m1() {", - " System.out.println(\"foo\");", - " }", - "", - " static int TWO = 2;", - "", - " {", - " System.out.println(\"I'm an initializer block!\");", - " }", - "", - " static {", - " System.out.println(\"I'm a static initializer block!\");", - " }", - "}") - .addOutputLines( - "A.java", - "class A {", - " private static final int X = 1;", - " private static String FOO = \"foo\";", - "", - " static int ONE = 1;", - "", - " private static String BAR = \"bar\";", - "", - " static int TWO = 2;", - "", - " char a = 'a';", - " char b = 'b';", - "", - " static {", - " System.out.println(\"I'm a static initializer block!\");", - " }", - "", - " {", - " System.out.println(\"I'm an initializer block!\");", - " }", - "", - " public A() {}", - "", - " void m2() {}", - "", - " void m1() {", - " System.out.println(\"foo\");", - " }", - "", - " interface InnerInterface {}", - "", - " class InnerClass {}", - "", - " static class StaticInnerClass {}", - "}") - .doTest(TestMode.TEXT_MATCH); - } - - @Test - void replacementSuggestedFixConsidersDefaultConstructor() { - BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) - .addInputLines( - "A.java", - "class A {", - " void m1() {}", - "", - " char c = 'c';", - "", - " private static final String foo = \"foo\";", - "", - " static int one = 1;", - "}") - .addOutputLines( - "A.java", - "class A {", - " private static final String foo = \"foo\";", - "", - " static int one = 1;", - "", - " char c = 'c';", - "", - " void m1() {}", - "}") - .doTest(TestMode.TEXT_MATCH); - } - - @SuppressWarnings("ErrorProneTestHelperSourceFormat") - @Test - void replacementSuggestedFixConsidersComments() { - BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) - .addInputLines( - "A.java", - "class A {", - " // detached comment from method", - " ;void method1() {}", - "", - " // first comment prior to method", - " // second comment prior to method", - " void method2() {", - " // Print line 'foo' to stdout.", - " System.out.println(\"foo\");", - " }", - "", - " // foo", - " /** Instantiates a new {@link A} instance. */", - " public A() {}", - "}") - .addOutputLines( - "A.java", - "class A {", - " // foo", - " /** Instantiates a new {@link A} instance. */", - " public A() {}", - "", - " // detached comment from method", - " void method1() {}", - "", - " // first comment prior to method", - " // second comment prior to method", - " void method2() {", - " // Print line 'foo' to stdout.", - " System.out.println(\"foo\");", - " }", - "}") - .doTest(TestMode.TEXT_MATCH); - } - - @Test - void replacementSuggestedFixConsidersAnnotations() { - BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) - .addInputLines( - "A.java", - "class A {", - " @SuppressWarnings(\"foo\")", - " void m1() {}", - "", - " @SuppressWarnings(\"bar\")", - " A() {}", - "}") - .addOutputLines( - "A.java", - "class A {", - " @SuppressWarnings(\"bar\")", - " A() {}", - "", - " @SuppressWarnings(\"foo\")", - " void m1() {}", - "}") - .doTest(TestMode.TEXT_MATCH); - } - - @SuppressWarnings("ErrorProneTestHelperSourceFormat") - @Test - void replacementSuggestedFixDoesNotModifyWhitespace() { - BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) - .addInputLines( - "A.java", - "", - "", - "class A {", - "", - "", - " // `m1()` comment.", - " void m1() {", - " // Print line 'foo' to stdout.", - " System.out.println(\"foo\");", - " }", - " public A () { }", - "", - "", - "}") - .addOutputLines( - "A.java", - "", - "", - "class A {", - "", - "", - "", - " public A () { }", - " // `m1()` comment.", - " void m1() {", - " // Print line 'foo' to stdout.", - " System.out.println(\"foo\");", - " }", - "", - "", - "}") - .doTest(TestMode.TEXT_MATCH); - } - - // XXX: This test should fail, if we verify that whitespace is preserved. - @SuppressWarnings("ErrorProneTestHelperSourceFormat") - void xxx() { - BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrdering.class, getClass()) - .addInputLines( - "A.java", - "", - "", - "class A {", - "", - "", - " // `m1()` comment.", - " void m1() {", - " // Print line 'foo' to stdout.", - " System.out.println(\"foo\");", - " }", - " public A () { }", - "", - "", - "}") - .addOutputLines( - "A.java", - "", - "", - "class A {", - "", - " ", - " ", - " \t \t", - " ", - " ", - "", - " public A () { }", - " // `m1()` comment.", - " void m1", - " ()", - " {", - " // Print line 'foo' to stdout.", - " System.out.println(\"foo\");", - " }", - "", - "", - "}") - .doTest(TestMode.TEXT_MATCH); - } -} From 4e33c52fea1255992359c858d9b0ab1239c9099a Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 22 Feb 2024 12:54:20 +0100 Subject: [PATCH 34/51] Post rebase fix --- .../bugpatterns/TypeMemberOrder.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index e7035f5f75..4b113feb5d 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -9,7 +9,7 @@ import static java.util.Comparator.comparing; import static java.util.Comparator.naturalOrder; import static java.util.Objects.requireNonNull; -import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; +import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL; import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; @@ -122,21 +122,15 @@ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { if (!canMove(tree, state)) { return Optional.empty(); } - switch (tree.getKind()) { - case VARIABLE: - return Optional.of(isStatic((VariableTree) tree) ? 1 : 2); - case BLOCK: - return Optional.of(isStatic((BlockTree) tree) ? 3 : 4); - case METHOD: - return Optional.of(isConstructor((MethodTree) tree) ? 5 : 6); - case CLASS: - case INTERFACE: - case ENUM: - return Optional.of(7); - default: - // TODO: Should we log unhandled kinds? - return Optional.empty(); - } + return switch (tree.getKind()) { + case VARIABLE -> Optional.of(isStatic((VariableTree) tree) ? 1 : 2); + case BLOCK -> Optional.of(isStatic((BlockTree) tree) ? 3 : 4); + case METHOD -> Optional.of(isConstructor((MethodTree) tree) ? 5 : 6); + case CLASS, INTERFACE, ENUM -> Optional.of(7); + default -> + // TODO: Should we log unhandled kinds? + Optional.empty(); + }; } private boolean canMove(Tree tree, VisitorState state) { @@ -217,7 +211,7 @@ private static SuggestedFix sortTypeMembers( member.tree(), start, end, - member.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ ))); + member.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */))); } start = end; } From fb14933214658e435413b2d75fc69d6a14e9a6d0 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 22 Feb 2024 13:43:09 +0100 Subject: [PATCH 35/51] Pair programming session --- .../bugpatterns/TypeMemberOrder.java | 26 +++++++++---------- .../TypeMemberOrderInterfaceTest.java | 1 + 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 4b113feb5d..27b64223ef 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -20,6 +20,7 @@ import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.Var; import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; @@ -29,9 +30,11 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; import com.sun.tools.javac.parser.Tokens.TokenKind; import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; import com.sun.tools.javac.util.Position; import java.util.ArrayList; import java.util.List; @@ -67,7 +70,7 @@ linkType = CUSTOM, severity = WARNING, tags = STYLE) -public final class TypeMemberOrder extends BugChecker implements BugChecker.ClassTreeMatcher { +public final class TypeMemberOrder extends BugChecker implements ClassTreeMatcher { private static final long serialVersionUID = 1L; /** Instantiates a new {@link TypeMemberOrder} instance. */ @@ -75,9 +78,8 @@ public TypeMemberOrder() {} @Override public Description matchClass(ClassTree tree, VisitorState state) { - if (tree.getKind() != Tree.Kind.CLASS - && tree.getKind() != Tree.Kind.INTERFACE - && tree.getKind() != Tree.Kind.ENUM) { + Kind kind = tree.getKind(); + if (kind != Kind.CLASS && kind != Kind.INTERFACE && kind != Kind.ENUM) { return Description.NO_MATCH; } @@ -85,6 +87,7 @@ public Description matchClass(ClassTree tree, VisitorState state) { // present in-between members. ImmutableList members = tree.getMembers().stream() + // XXX: Maybe we can remove this filter? .filter(TypeMemberOrder::hasSource) .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) .collect(toImmutableList()); @@ -127,9 +130,7 @@ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { case BLOCK -> Optional.of(isStatic((BlockTree) tree) ? 3 : 4); case METHOD -> Optional.of(isConstructor((MethodTree) tree) ? 5 : 6); case CLASS, INTERFACE, ENUM -> Optional.of(7); - default -> - // TODO: Should we log unhandled kinds? - Optional.empty(); + default -> Optional.empty(); }; } @@ -138,21 +139,20 @@ private boolean canMove(Tree tree, VisitorState state) { } private static boolean hasSource(Tree tree) { - if (tree.getKind() == Tree.Kind.METHOD) { + if (tree.getKind() == Kind.METHOD) { return !ASTHelpers.isGeneratedConstructor(((MethodTree) tree)); } return true; } /** - * Returns true if Tree is an enumerator of an enumerated type, or false otherwise. + * Returns true if `Tree` is an enumerator of an enumerated type, or false otherwise. * * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) * @see com.sun.tools.javac.code.Flags#ENUM */ private static boolean isEnumerator(Tree tree) { - return tree instanceof JCTree.JCVariableDecl - && (((JCTree.JCVariableDecl) tree).mods.flags & ENUM) != 0; + return tree instanceof JCVariableDecl && (((JCVariableDecl) tree).mods.flags & ENUM) != 0; } /** @@ -174,7 +174,7 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { .dropWhile(token -> token.kind() != TokenKind.LBRACE) // XXX: To accommodate enums, start processing the body, after the enumerated type's // enumerators. - .dropWhile(token -> tree.getKind() == Tree.Kind.ENUM && token.kind() != TokenKind.SEMI) + .dropWhile(token -> tree.getKind() == Kind.ENUM && token.kind() != TokenKind.SEMI) .findFirst() .map(ErrorProneToken::endPos) .orElse(Position.NOPOS); @@ -211,7 +211,7 @@ private static SuggestedFix sortTypeMembers( member.tree(), start, end, - member.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */))); + member.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ ))); } start = end; } diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java index 9647d1fb57..9dbaca7984 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java @@ -5,6 +5,7 @@ import com.google.errorprone.CompilationTestHelper; import org.junit.jupiter.api.Test; +// XXX: Add test case for abstract class and default methods. final class TypeMemberOrderInterfaceTest { @Test void identification() { From d4441062f660d7a717fbc0aaf3744cfd7e9fa4c3 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 3 Mar 2024 09:25:59 +0100 Subject: [PATCH 36/51] Test abstract and default methods --- .../bugpatterns/TypeMemberOrder.java | 16 ++++++--- .../bugpatterns/TypeMemberOrderClassTest.java | 33 ++++++++++++++++++ .../TypeMemberOrderInterfaceTest.java | 34 ++++++++++++++++++- .../TypeMemberOrderUnhandledKindsTest.java | 32 +++++++++++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledKindsTest.java diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 27b64223ef..fe1d90a8e8 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -87,13 +87,12 @@ public Description matchClass(ClassTree tree, VisitorState state) { // present in-between members. ImmutableList members = tree.getMembers().stream() - // XXX: Maybe we can remove this filter? .filter(TypeMemberOrder::hasSource) .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) .collect(toImmutableList()); // List of the sortable members' preferred ordinals, - // ordered by the member's position in original source. + // ordered by the member's position in the original source. ImmutableList preferredOrdinals = members.stream() .filter(m -> m.preferredOrdinal().isPresent()) @@ -130,6 +129,7 @@ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { case BLOCK -> Optional.of(isStatic((BlockTree) tree) ? 3 : 4); case METHOD -> Optional.of(isConstructor((MethodTree) tree) ? 5 : 6); case CLASS, INTERFACE, ENUM -> Optional.of(7); + // TODO: Should we log unhandled kinds? default -> Optional.empty(); }; } @@ -172,8 +172,14 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { sourceCode.subSequence(typeStart, typeEnd).toString(), typeStart, state.context) .stream() .dropWhile(token -> token.kind() != TokenKind.LBRACE) - // XXX: To accommodate enums, start processing the body, after the enumerated type's - // enumerators. + /* + * To accommodate enums, skip processing their enumerators. + * This is needed as ErrorProne has access to the enumerations individually, but not to the + * whole expression that declares them, leaving the semicolon trailing the declarations + * unaccounted for. The current logic would move this trailing semicolon with the first + * member after the enumerations instead of leaving it to close the enumerations' + * declaration, introducing a syntax error. + */ .dropWhile(token -> tree.getKind() == Kind.ENUM && token.kind() != TokenKind.SEMI) .findFirst() .map(ErrorProneToken::endPos) @@ -198,7 +204,7 @@ private static SuggestedFix sortTypeMembers( for (TypeMember member : members) { int end = state.getEndPosition(member.tree()); if (isEnumerator(member.tree())) { - // XXX: To accommodate enums, skip enumerators of enumerated types. + // To accommodate enums, skip processing their enumerators. continue; } verify( diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java index 1abd7a7190..1fcdbca0ba 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java @@ -136,6 +136,39 @@ void replacementSuggestedFix() { .doTest(TestMode.TEXT_MATCH); } + @Test + void replacementSuggestedFixAbstractMethods() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "abstract class A {", + " static class InnerClass {}", + "", + " void foo() {}", + "", + " abstract void bar();", + "", + " void baz() {}", + "", + " A() {}", + "}") + .addOutputLines( + "A.java", + "abstract class A {", + "", + " A() {}", + "", + " void foo() {}", + "", + " abstract void bar();", + "", + " void baz() {}", + "", + " static class InnerClass {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + @Test void replacementSuggestedFixConsidersUnmovableMembers() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java index 9dbaca7984..1919f0d3de 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java @@ -5,7 +5,6 @@ import com.google.errorprone.CompilationTestHelper; import org.junit.jupiter.api.Test; -// XXX: Add test case for abstract class and default methods. final class TypeMemberOrderInterfaceTest { @Test void identification() { @@ -93,6 +92,39 @@ void replacementSuggestedFix() { .doTest(TestMode.TEXT_MATCH); } + @Test + void replacementSuggestedFixDefaultMethods() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "interface A {", + " class InnerClass {}", + "", + " void foo();", + "", + " default void bar() {}", + "", + " void baz();", + "", + " static final int QUX = 1;", + "}") + .addOutputLines( + "A.java", + "interface A {", + "", + " static final int QUX = 1;", + "", + " void foo();", + "", + " default void bar() {}", + "", + " void baz();", + "", + " class InnerClass {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + @Test void replacementSuggestedFixConsidersUnmovableMembers() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledKindsTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledKindsTest.java new file mode 100644 index 0000000000..af11649bc0 --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledKindsTest.java @@ -0,0 +1,32 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +final class TypeMemberOrderUnhandledKindsTest { + @Test + void identification() { + CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addSourceLines( + "A.java", + "@interface A {", + " class InnerClass_1 {}", + "", + " int foo();", + "", + " int bar = 0;", + "", + " class InnerClass_2 {}", + "}") + .addSourceLines( + "B.java", + "record B(int foo, int bar) {", + " void baz() {}", + "", + " static final int QUX = 1;", + "", + " void quux() {}", + "}") + .doTest(); + } +} From ecd91d17cb74f79d51a82ad907bf1a380218af9c Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Mon, 4 Mar 2024 10:08:24 +0100 Subject: [PATCH 37/51] Tweaks --- .../bugpatterns/TypeMemberOrder.java | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index fe1d90a8e8..4dd1593be7 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -83,16 +83,15 @@ public Description matchClass(ClassTree tree, VisitorState state) { return Description.NO_MATCH; } - // Keep track of all members, including unmovable ones, to exclude them from the sources - // present in-between members. + // All members that can be moved or may lay between movable ones. ImmutableList members = tree.getMembers().stream() - .filter(TypeMemberOrder::hasSource) + .filter(member -> isGeneratedConstructor(member) && !isEnumerator(member)) .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) .collect(toImmutableList()); - // List of the sortable members' preferred ordinals, - // ordered by the member's position in the original source. + // List of the sortable members' preferred ordinals, ordered by the member's position in the + // original source. ImmutableList preferredOrdinals = members.stream() .filter(m -> m.preferredOrdinal().isPresent()) @@ -116,12 +115,29 @@ public Description matchClass(ClassTree tree, VisitorState state) { return describeMatch(tree, sortTypeMembers(bodyStartPos, members, state)); } + private static boolean isGeneratedConstructor(Tree tree) { + if (tree.getKind() == Kind.METHOD) { + return !ASTHelpers.isGeneratedConstructor(((MethodTree) tree)); + } + return true; + } + + /** + * Returns true if `Tree` is an enumerator of an enumerated type, or false otherwise. + * + * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) + * @see com.sun.tools.javac.code.Flags#ENUM + */ + private static boolean isEnumerator(Tree tree) { + return tree instanceof JCVariableDecl && (((JCVariableDecl) tree).mods.flags & ENUM) != 0; + } + /** * Returns the preferred ordinal of the given member, or empty if it's unmovable for any reason, * including it lacking a preferred ordinal. */ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { - if (!canMove(tree, state)) { + if (isSuppressed(tree, state)) { return Optional.empty(); } return switch (tree.getKind()) { @@ -134,27 +150,6 @@ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { }; } - private boolean canMove(Tree tree, VisitorState state) { - return hasSource(tree) && !isSuppressed(tree, state) && !isEnumerator(tree); - } - - private static boolean hasSource(Tree tree) { - if (tree.getKind() == Kind.METHOD) { - return !ASTHelpers.isGeneratedConstructor(((MethodTree) tree)); - } - return true; - } - - /** - * Returns true if `Tree` is an enumerator of an enumerated type, or false otherwise. - * - * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) - * @see com.sun.tools.javac.code.Flags#ENUM - */ - private static boolean isEnumerator(Tree tree) { - return tree instanceof JCVariableDecl && (((JCVariableDecl) tree).mods.flags & ENUM) != 0; - } - /** * Returns the start position of the body of the given type, in the case of enums, it returns the * position that follows the enumerated type's enumerations. @@ -203,10 +198,6 @@ private static SuggestedFix sortTypeMembers( @Var int start = bodyStartPos; for (TypeMember member : members) { int end = state.getEndPosition(member.tree()); - if (isEnumerator(member.tree())) { - // To accommodate enums, skip processing their enumerators. - continue; - } verify( end != Position.NOPOS && start < end, "Unexpected member end position, member: %s", From 00bd2475de5efbcaced6308bfd5179f109bf1b96 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Sat, 23 Mar 2024 20:56:49 +0100 Subject: [PATCH 38/51] Suggestions --- .../bugpatterns/TypeMemberOrder.java | 33 ++++++++----------- .../bugpatterns/TypeMemberOrderClassTest.java | 12 +++---- .../bugpatterns/TypeMemberOrderEnumTest.java | 10 +++--- .../TypeMemberOrderInterfaceTest.java | 10 +++--- .../errorprone/utils/MoreASTHelpers.java | 10 ++++++ 5 files changed, 40 insertions(+), 35 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 4dd1593be7..5b9785f0f3 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -41,6 +41,7 @@ import java.util.Optional; import java.util.Set; import javax.lang.model.element.Modifier; +import tech.picnic.errorprone.utils.MoreASTHelpers; /** * A {@link BugChecker} that flags classes with a non-canonical member order. @@ -61,11 +62,9 @@ * href="https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html">Checkstyle's * {@code DeclarationOrderCheck} */ -// XXX: Reference -// https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html @AutoService(BugChecker.class) @BugPattern( - summary = "Type members should be ordered in a standard way", + summary = "Type members should be defined in a canonical order", link = BUG_PATTERNS_BASE_URL + "TypeMemberOrder", linkType = CUSTOM, severity = WARNING, @@ -78,20 +77,23 @@ public TypeMemberOrder() {} @Override public Description matchClass(ClassTree tree, VisitorState state) { - Kind kind = tree.getKind(); - if (kind != Kind.CLASS && kind != Kind.INTERFACE && kind != Kind.ENUM) { + Kind treeKind = tree.getKind(); + if (treeKind != Kind.CLASS && treeKind != Kind.INTERFACE && treeKind != Kind.ENUM) { return Description.NO_MATCH; } - // All members that can be moved or may lay between movable ones. + /* All members that can be moved or may lay between movable ones. */ ImmutableList members = tree.getMembers().stream() - .filter(member -> isGeneratedConstructor(member) && !isEnumerator(member)) + .filter( + member -> !MoreASTHelpers.isGeneratedConstructor(member) && !isEnumerator(member)) .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) .collect(toImmutableList()); - // List of the sortable members' preferred ordinals, ordered by the member's position in the - // original source. + /* + List of the sortable members' preferred ordinals, ordered by the member's position in the + original source. + */ ImmutableList preferredOrdinals = members.stream() .filter(m -> m.preferredOrdinal().isPresent()) @@ -115,21 +117,14 @@ public Description matchClass(ClassTree tree, VisitorState state) { return describeMatch(tree, sortTypeMembers(bodyStartPos, members, state)); } - private static boolean isGeneratedConstructor(Tree tree) { - if (tree.getKind() == Kind.METHOD) { - return !ASTHelpers.isGeneratedConstructor(((MethodTree) tree)); - } - return true; - } - /** - * Returns true if `Tree` is an enumerator of an enumerated type, or false otherwise. + * Returns true if {@link Tree} is an enumerator of an enumerated type, false otherwise. * * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) * @see com.sun.tools.javac.code.Flags#ENUM */ private static boolean isEnumerator(Tree tree) { - return tree instanceof JCVariableDecl && (((JCVariableDecl) tree).mods.flags & ENUM) != 0; + return tree instanceof JCVariableDecl variableDecl && (variableDecl.mods.flags & ENUM) != 0; } /** @@ -162,7 +157,7 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { return Position.NOPOS; } - // We return the source code position of the first token that follows the first left brace. + /* We return the source code position of the first token that follows the first left brace. */ return ErrorProneTokens.getTokens( sourceCode.subSequence(typeStart, typeEnd).toString(), typeStart, state.context) .stream() diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java index 1fcdbca0ba..5c6c5877cb 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java @@ -11,7 +11,7 @@ void identification() { CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) .expectErrorMessage( "TypeMemberOrder", - message -> message.contains("Type members should be ordered in a standard way")) + message -> message.contains("Type members should be ordered in a canonical order")) .addSourceLines( "A.java", "// BUG: Diagnostic matches: TypeMemberOrder", @@ -86,7 +86,7 @@ void identification() { } @Test - void replacementSuggestedFix() { + void replacement() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -137,7 +137,7 @@ void replacementSuggestedFix() { } @Test - void replacementSuggestedFixAbstractMethods() { + void replacementAbstractMethods() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -170,7 +170,7 @@ void replacementSuggestedFixAbstractMethods() { } @Test - void replacementSuggestedFixConsidersUnmovableMembers() { + void replacementUnmovableMembers() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -218,7 +218,7 @@ void replacementSuggestedFixConsidersUnmovableMembers() { } @Test - void replacementSuggestedFixConsidersDefaultConstructor() { + void replacementDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -261,7 +261,7 @@ void replacementSuggestedFixConsidersDefaultConstructor() { } @Test - void replacementSuggestedFixConsidersDanglingComments() { + void replacementDanglingComments() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java index b50e4f680e..8cc7e6b01a 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java @@ -11,7 +11,7 @@ void identification() { CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) .expectErrorMessage( "TypeMemberOrder", - message -> message.contains("Type members should be ordered in a standard way")) + message -> message.contains("Type members should be ordered in a canonical order")) .addSourceLines( "A.java", "// BUG: Diagnostic matches: TypeMemberOrder", @@ -89,7 +89,7 @@ void identification() { } @Test - void replacementSuggestedFix() { + void replacement() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -147,7 +147,7 @@ void replacementSuggestedFix() { } @Test - void replacementSuggestedFixConsidersUnmovableMembers() { + void replacementUnmovableMembers() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -211,7 +211,7 @@ void replacementSuggestedFixConsidersUnmovableMembers() { } @Test - void replacementSuggestedFixConsidersDefaultConstructor() { + void replacementDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -265,7 +265,7 @@ void replacementSuggestedFixConsidersDefaultConstructor() { } @Test - void replacementSuggestedFixConsidersDanglingComments() { + void replacementDanglingComments() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java index 1919f0d3de..7a470577d1 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java @@ -11,7 +11,7 @@ void identification() { CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) .expectErrorMessage( "TypeMemberOrder", - message -> message.contains("Type members should be ordered in a standard way")) + message -> message.contains("Type members should be ordered in a canonical order")) .addSourceLines( "A.java", "// BUG: Diagnostic matches: TypeMemberOrder", @@ -60,7 +60,7 @@ void identification() { } @Test - void replacementSuggestedFix() { + void replacement() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -93,7 +93,7 @@ void replacementSuggestedFix() { } @Test - void replacementSuggestedFixDefaultMethods() { + void replacementDefaultMethods() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -126,7 +126,7 @@ void replacementSuggestedFixDefaultMethods() { } @Test - void replacementSuggestedFixConsidersUnmovableMembers() { + void replacementUnmovableMembers() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -166,7 +166,7 @@ void replacementSuggestedFixConsidersUnmovableMembers() { } @Test - void replacementSuggestedFixConsidersDanglingComments() { + void replacementDanglingComments() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", diff --git a/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java b/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java index 0caa0002fe..b6f2762023 100644 --- a/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java +++ b/error-prone-utils/src/main/java/tech/picnic/errorprone/utils/MoreASTHelpers.java @@ -79,6 +79,16 @@ public static boolean areSameType(Tree treeA, Tree treeB, VisitorState state) { return ASTHelpers.isSameType(ASTHelpers.getType(treeA), ASTHelpers.getType(treeB), state); } + /** + * Tells whether a given tree is a generated constructor. + * + * @param tree The tree of interest. + * @return Whether the specified tree is a generated constructor. + */ + public static boolean isGeneratedConstructor(Tree tree) { + return tree instanceof MethodTree methodTree && ASTHelpers.isGeneratedConstructor(methodTree); + } + /** * Tells whether the given tree is of type {@link String}. * From 8f171a5cf7a9d8184c35aa47b995ee83ee88ca5c Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Mon, 8 Apr 2024 09:11:33 +0200 Subject: [PATCH 39/51] Improve more tests --- .../errorprone/bugpatterns/TypeMemberOrderClassTest.java | 5 +---- .../errorprone/bugpatterns/TypeMemberOrderEnumTest.java | 5 +---- .../errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java index 5c6c5877cb..f85c4ebb3d 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java @@ -9,12 +9,9 @@ final class TypeMemberOrderClassTest { @Test void identification() { CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) - .expectErrorMessage( - "TypeMemberOrder", - message -> message.contains("Type members should be ordered in a canonical order")) .addSourceLines( "A.java", - "// BUG: Diagnostic matches: TypeMemberOrder", + "// BUG: Diagnostic contains:", "class A {", " class InnerClass {}", "", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java index 8cc7e6b01a..a227aedc54 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java @@ -9,12 +9,9 @@ final class TypeMemberOrderEnumTest { @Test void identification() { CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) - .expectErrorMessage( - "TypeMemberOrder", - message -> message.contains("Type members should be ordered in a canonical order")) .addSourceLines( "A.java", - "// BUG: Diagnostic matches: TypeMemberOrder", + "// BUG: Diagnostic contains:", "enum A {", " FOO;", "", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java index 7a470577d1..9351d3cd84 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java @@ -9,12 +9,9 @@ final class TypeMemberOrderInterfaceTest { @Test void identification() { CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) - .expectErrorMessage( - "TypeMemberOrder", - message -> message.contains("Type members should be ordered in a canonical order")) .addSourceLines( "A.java", - "// BUG: Diagnostic matches: TypeMemberOrder", + "// BUG: Diagnostic contains:", "interface A {", " static class InnerClass {}", "", From d0b94795cf00a33dd0a912ffbed3b1342c39620f Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sat, 4 May 2024 08:11:00 +0200 Subject: [PATCH 40/51] Reproduce syntactically wrong replacement with annotations containing `LBRACE` --- .../bugpatterns/TypeMemberOrderClassTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java index f85c4ebb3d..5c77899d22 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java @@ -315,4 +315,38 @@ void replacementDanglingComments() { "}") .doTest(TestMode.TEXT_MATCH); } + + @Test + void replacementComplexAnnotation() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "final class A {", + "", + " @interface AnnotationWithClassReferences {", + " Class[] value() default {};", + " }", + "", + " @AnnotationWithClassReferences({Object.class})", + " class InnerClass {", + " String bar;", + " private static final int foo = 1;", + " }", + "}") + .addOutputLines( + "A.java", + "final class A {", + "", + " @interface AnnotationWithClassReferences {", + " Class[] value() default {};", + " }", + "", + " @AnnotationWithClassReferences({Object.class})", + " class InnerClass {", + " private static final int foo = 1;", + " String bar;", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } } From 5652cb6991bc0010df3ed84b65625276c6326777 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sat, 4 May 2024 08:28:58 +0200 Subject: [PATCH 41/51] Fix syntactically wrong replecement with annotations containing `LBRACE` --- .../tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 5b9785f0f3..3170b17b9d 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -151,7 +151,8 @@ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { */ private static int getBodyStartPos(ClassTree tree, VisitorState state) { CharSequence sourceCode = state.getSourceCode(); - int typeStart = ASTHelpers.getStartPosition(tree); + /* To avoid including the type's preceding annotations, use `getPreferredPosition()` rather than ASTHelpers. */ + int typeStart = ((JCTree.JCClassDecl) tree).getPreferredPosition(); int typeEnd = state.getEndPosition(tree); if (sourceCode == null || typeStart == Position.NOPOS || typeEnd == Position.NOPOS) { return Position.NOPOS; From 173aebfdf12602effd87a8c1ff063b228e765a16 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Mon, 27 May 2024 08:49:00 +0200 Subject: [PATCH 42/51] Suggestions --- .../bugpatterns/TypeMemberOrder.java | 77 ++++++++++--------- .../bugpatterns/TypeMemberOrderClassTest.java | 48 +++++++++--- .../bugpatterns/TypeMemberOrderEnumTest.java | 2 +- .../TypeMemberOrderInterfaceTest.java | 12 +-- ...=> TypeMemberOrderUnhandledCasesTest.java} | 2 +- 5 files changed, 88 insertions(+), 53 deletions(-) rename error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/{TypeMemberOrderUnhandledKindsTest.java => TypeMemberOrderUnhandledCasesTest.java} (94%) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 3170b17b9d..65b45d25c1 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -82,28 +82,6 @@ public Description matchClass(ClassTree tree, VisitorState state) { return Description.NO_MATCH; } - /* All members that can be moved or may lay between movable ones. */ - ImmutableList members = - tree.getMembers().stream() - .filter( - member -> !MoreASTHelpers.isGeneratedConstructor(member) && !isEnumerator(member)) - .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) - .collect(toImmutableList()); - - /* - List of the sortable members' preferred ordinals, ordered by the member's position in the - original source. - */ - ImmutableList preferredOrdinals = - members.stream() - .filter(m -> m.preferredOrdinal().isPresent()) - .map(m -> m.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ )) - .collect(toImmutableList()); - - if (Comparators.isInOrder(preferredOrdinals, naturalOrder())) { - return Description.NO_MATCH; - } - int bodyStartPos = getBodyStartPos(tree, state); if (bodyStartPos == Position.NOPOS) { /* @@ -114,17 +92,22 @@ public Description matchClass(ClassTree tree, VisitorState state) { return Description.NO_MATCH; } + ImmutableList members = getAllTypeMembers(tree, state); + ImmutableList preferredOrdinals = extractPreferredOrdinals(members); + + if (Comparators.isInOrder(preferredOrdinals, naturalOrder())) { + return Description.NO_MATCH; + } + return describeMatch(tree, sortTypeMembers(bodyStartPos, members, state)); } - /** - * Returns true if {@link Tree} is an enumerator of an enumerated type, false otherwise. - * - * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) - * @see com.sun.tools.javac.code.Flags#ENUM - */ - private static boolean isEnumerator(Tree tree) { - return tree instanceof JCVariableDecl variableDecl && (variableDecl.mods.flags & ENUM) != 0; + /** Returns all members that can be moved or may lay between movable ones. */ + private ImmutableList getAllTypeMembers(ClassTree tree, VisitorState state) { + return tree.getMembers().stream() + .filter(member -> !MoreASTHelpers.isGeneratedConstructor(member) && !isEnumerator(member)) + .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) + .collect(toImmutableList()); } /** @@ -140,7 +123,6 @@ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { case BLOCK -> Optional.of(isStatic((BlockTree) tree) ? 3 : 4); case METHOD -> Optional.of(isConstructor((MethodTree) tree) ? 5 : 6); case CLASS, INTERFACE, ENUM -> Optional.of(7); - // TODO: Should we log unhandled kinds? default -> Optional.empty(); }; } @@ -158,14 +140,14 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { return Position.NOPOS; } - /* We return the source code position of the first token that follows the first left brace. */ + /* Returns the source code position of the first token that comes after the first curly left bracket. */ return ErrorProneTokens.getTokens( sourceCode.subSequence(typeStart, typeEnd).toString(), typeStart, state.context) .stream() .dropWhile(token -> token.kind() != TokenKind.LBRACE) /* * To accommodate enums, skip processing their enumerators. - * This is needed as ErrorProne has access to the enumerations individually, but not to the + * This is needed as Error Prone has access to the enumerations individually, but not to the * whole expression that declares them, leaving the semicolon trailing the declarations * unaccounted for. The current logic would move this trailing semicolon with the first * member after the enumerations instead of leaving it to close the enumerations' @@ -177,6 +159,28 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { .orElse(Position.NOPOS); } + /** + * Returns a list of the sortable members' preferred ordinals, ordered by the member's position in + * the original source. + */ + private static ImmutableList extractPreferredOrdinals( + ImmutableList members) { + return members.stream() + .filter(m -> m.preferredOrdinal().isPresent()) + .map(m -> m.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ )) + .collect(toImmutableList()); + } + + /** + * Returns true if {@link Tree} is an enumerator of an enumerated type, false otherwise. + * + * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) + * @see com.sun.tools.javac.code.Flags#ENUM + */ + private static boolean isEnumerator(Tree tree) { + return tree instanceof JCVariableDecl variableDecl && (variableDecl.mods.flags & ENUM) != 0; + } + /** * Suggests a different way of ordering the given type members. * @@ -196,8 +200,9 @@ private static SuggestedFix sortTypeMembers( int end = state.getEndPosition(member.tree()); verify( end != Position.NOPOS && start < end, - "Unexpected member end position, member: %s", - member); + "Unexpected member end position, member: %s, end position: %s", + member, + end); if (member.preferredOrdinal().isPresent()) { membersWithSource.add( new AutoValue_TypeMemberOrder_MovableTypeMember( @@ -232,6 +237,7 @@ private static boolean isConstructor(MethodTree methodTree) { return ASTHelpers.getSymbol(methodTree).isConstructor(); } + /** XXX: Write this. Every member that is in a ClassTree? */ @AutoValue abstract static class TypeMember { abstract Tree tree(); @@ -239,6 +245,7 @@ abstract static class TypeMember { abstract Optional preferredOrdinal(); } + /** Type members that have a sourcecode and are not generated, are considered to be movable. */ @AutoValue abstract static class MovableTypeMember { abstract Tree tree(); diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java index 5c77899d22..49f8695562 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java @@ -78,7 +78,15 @@ void identification() { "", " D() {}", "}") - .addSourceLines("E.java", "class E {}") + .addSourceLines( + "E.java", + "@SuppressWarnings(\"TypeMemberOrder\")", + "class E {", + " void unorderedMethod() {}", + "", + " E() {}", + "}") + .addSourceLines("F.java", "class F {}") .doTest(); } @@ -215,7 +223,7 @@ void replacementUnmovableMembers() { } @Test - void replacementDefaultConstructor() { + void replacementHandlesGeneratedDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", @@ -263,9 +271,13 @@ void replacementDanglingComments() { .addInputLines( "A.java", "class A {", - " /* `foo` method's dangling comment. */", + " /* empty statement's dangling comment */", " ;", - " // `foo` method's comment", + " /**", + " * Multiline JavaDoc", + " *", + " *

`foo` method's comment", + " */", " int foo() {", " return foo;", " }", @@ -279,7 +291,7 @@ void replacementDanglingComments() { " static {", " System.out.println(\"foo\");", " }", - " /* `bar` field's dangling comment. */ ;", + " /* `bar` field's dangling comment */", "", " private final int bar = 2;", " // `foo` field's comment", @@ -291,7 +303,7 @@ void replacementDanglingComments() { "class A {", " // `foo` field's comment", " private static final int foo = 1;", - " /* `bar` field's dangling comment. */ ;", + " /* `bar` field's dangling comment */", "", " private final int bar = 2;", "", @@ -304,10 +316,14 @@ void replacementDanglingComments() { " {", " System.out.println(\"bar\");", " }", - " /* `foo` method's dangling comment. */", + " /* empty statement's dangling comment */", " ;", "", - " // `foo` method's comment", + " /**", + " * Multiline JavaDoc", + " *", + " *

`foo` method's comment", + " */", " int foo() {", " return foo;", " }", @@ -328,7 +344,13 @@ void replacementComplexAnnotation() { " }", "", " @AnnotationWithClassReferences({Object.class})", - " class InnerClass {", + " class InnerClassOneValue {", + " String bar;", + " private static final int foo = 1;", + " }", + "", + " @AnnotationWithClassReferences(value = {Integer.class, String.class})", + " class InnerClassTwoValues {", " String bar;", " private static final int foo = 1;", " }", @@ -342,7 +364,13 @@ void replacementComplexAnnotation() { " }", "", " @AnnotationWithClassReferences({Object.class})", - " class InnerClass {", + " class InnerClassOneValue {", + " private static final int foo = 1;", + " String bar;", + " }", + "", + " @AnnotationWithClassReferences(value = {Integer.class, String.class})", + " class InnerClassTwoValues {", " private static final int foo = 1;", " String bar;", " }", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java index a227aedc54..e72ae93827 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java @@ -208,7 +208,7 @@ void replacementUnmovableMembers() { } @Test - void replacementDefaultConstructor() { + void replacementHandlesGeneratedDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java index 9351d3cd84..5186efc15a 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java @@ -168,22 +168,22 @@ void replacementDanglingComments() { .addInputLines( "A.java", "interface A {", - " // `bar` method's dangling comment.", + " // empty statement's dangling comment", " ;", " // `bar` method's comment", " void bar();", "", - " // `foo` method's comment", - " static final int foo = 1;", + " // `foo` field's comment", + " int foo = 1;", " // trailing comment", "}") .addOutputLines( "A.java", "interface A {", "", - " // `foo` method's comment", - " static final int foo = 1;", - " // `bar` method's dangling comment.", + " // `foo` field's comment", + " int foo = 1;", + " // empty statement's dangling comment", " ;", "", " // `bar` method's comment", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledKindsTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledCasesTest.java similarity index 94% rename from error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledKindsTest.java rename to error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledCasesTest.java index af11649bc0..dfd243586d 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledKindsTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledCasesTest.java @@ -3,7 +3,7 @@ import com.google.errorprone.CompilationTestHelper; import org.junit.jupiter.api.Test; -final class TypeMemberOrderUnhandledKindsTest { +final class TypeMemberOrderUnhandledCasesTest { @Test void identification() { CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) From 2ae6f20cde0087056634ab4d55ba64228931e159 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 30 May 2024 14:15:42 +0200 Subject: [PATCH 43/51] Suggestions part 1 --- .../bugpatterns/TypeMemberOrder.java | 41 +++++++++++++------ .../TypeMemberOrderUnhandledCasesTest.java | 32 --------------- 2 files changed, 28 insertions(+), 45 deletions(-) delete mode 100644 error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledCasesTest.java diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 65b45d25c1..2dc4c47e47 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -62,6 +62,11 @@ * href="https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html">Checkstyle's * {@code DeclarationOrderCheck} */ +// XXX: Consider introducing support for ordering members in records or annotation definitions. +// XXX: Merge the type members, go over all of them, check if they have a source code and then don't +// consider them. +// Make sure that we implement comparable and can just call "sort" on the list and they are ready. +// As a result we don't need the optionals anymore. @AutoService(BugChecker.class) @BugPattern( summary = "Type members should be defined in a canonical order", @@ -103,10 +108,12 @@ public Description matchClass(ClassTree tree, VisitorState state) { } /** Returns all members that can be moved or may lay between movable ones. */ + // XXX: Check if does have source code. private ImmutableList getAllTypeMembers(ClassTree tree, VisitorState state) { return tree.getMembers().stream() - .filter(member -> !MoreASTHelpers.isGeneratedConstructor(member) && !isEnumerator(member)) - .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getPreferredOrdinal(m, state))) + .filter( + member -> !MoreASTHelpers.isGeneratedConstructor(member) && !isEnumDefinition(member)) + .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getMemberTypeOrdinal(m, state))) .collect(toImmutableList()); } @@ -114,7 +121,8 @@ private ImmutableList getAllTypeMembers(ClassTree tree, VisitorState * Returns the preferred ordinal of the given member, or empty if it's unmovable for any reason, * including it lacking a preferred ordinal. */ - private Optional getPreferredOrdinal(Tree tree, VisitorState state) { + private Optional getMemberTypeOrdinal(Tree tree, VisitorState state) { + // Check hier ook die andere. if (isSuppressed(tree, state)) { return Optional.empty(); } @@ -133,25 +141,31 @@ private Optional getPreferredOrdinal(Tree tree, VisitorState state) { */ private static int getBodyStartPos(ClassTree tree, VisitorState state) { CharSequence sourceCode = state.getSourceCode(); - /* To avoid including the type's preceding annotations, use `getPreferredPosition()` rather than ASTHelpers. */ + /* + * To avoid including the type's preceding annotations, use `getPreferredPosition()` rather than + * `ASTHelpers`. + */ int typeStart = ((JCTree.JCClassDecl) tree).getPreferredPosition(); int typeEnd = state.getEndPosition(tree); if (sourceCode == null || typeStart == Position.NOPOS || typeEnd == Position.NOPOS) { return Position.NOPOS; } - /* Returns the source code position of the first token that comes after the first curly left bracket. */ + /* + * Returns the source code position of the first token that comes after the first curly left + * bracket. + */ return ErrorProneTokens.getTokens( sourceCode.subSequence(typeStart, typeEnd).toString(), typeStart, state.context) .stream() .dropWhile(token -> token.kind() != TokenKind.LBRACE) /* - * To accommodate enums, skip processing their enumerators. - * This is needed as Error Prone has access to the enumerations individually, but not to the - * whole expression that declares them, leaving the semicolon trailing the declarations - * unaccounted for. The current logic would move this trailing semicolon with the first - * member after the enumerations instead of leaving it to close the enumerations' - * declaration, introducing a syntax error. + * To accommodate enums, skip processing their enumerators. This is needed as Error Prone + * has access to the enumerations individually, but not to the whole expression that + * declares them, leaving the semicolon trailing the declarations unaccounted for. The + * current logic would move this trailing semicolon with the first member after the + * enumerations instead of leaving it to close the enumerations' declaration, introducing a + * syntax error. */ .dropWhile(token -> tree.getKind() == Kind.ENUM && token.kind() != TokenKind.SEMI) .findFirst() @@ -172,12 +186,12 @@ private static ImmutableList extractPreferredOrdinals( } /** - * Returns true if {@link Tree} is an enumerator of an enumerated type, false otherwise. + * Returns true if {@link Tree} is an enum or an enumerator definition, false otherwise. * * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) * @see com.sun.tools.javac.code.Flags#ENUM */ - private static boolean isEnumerator(Tree tree) { + private static boolean isEnumDefinition(Tree tree) { return tree instanceof JCVariableDecl variableDecl && (variableDecl.mods.flags & ENUM) != 0; } @@ -204,6 +218,7 @@ private static SuggestedFix sortTypeMembers( member, end); if (member.preferredOrdinal().isPresent()) { + // XXX: .isPresent(). Boven in ook doen, map naar optional. Filter isEmpty, mapNotNull? membersWithSource.add( new AutoValue_TypeMemberOrder_MovableTypeMember( member.tree(), diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledCasesTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledCasesTest.java deleted file mode 100644 index dfd243586d..0000000000 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderUnhandledCasesTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package tech.picnic.errorprone.bugpatterns; - -import com.google.errorprone.CompilationTestHelper; -import org.junit.jupiter.api.Test; - -final class TypeMemberOrderUnhandledCasesTest { - @Test - void identification() { - CompilationTestHelper.newInstance(TypeMemberOrder.class, getClass()) - .addSourceLines( - "A.java", - "@interface A {", - " class InnerClass_1 {}", - "", - " int foo();", - "", - " int bar = 0;", - "", - " class InnerClass_2 {}", - "}") - .addSourceLines( - "B.java", - "record B(int foo, int bar) {", - " void baz() {}", - "", - " static final int QUX = 1;", - "", - " void quux() {}", - "}") - .doTest(); - } -} From c521778b2ff4e47bcbc41e99a1169e5515f91b01 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 30 May 2024 18:03:35 +0200 Subject: [PATCH 44/51] Try to implement new thing --- .../bugpatterns/TypeMemberOrder.java | 117 ++++++++---------- .../bugpatterns/TypeMemberOrderEnumTest.java | 30 +++++ 2 files changed, 79 insertions(+), 68 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 2dc4c47e47..7cc59459f0 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -1,19 +1,15 @@ package tech.picnic.errorprone.bugpatterns; -import static com.google.common.base.Verify.verify; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.errorprone.BugPattern.LinkType.CUSTOM; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.BugPattern.StandardTags.STYLE; import static com.sun.tools.javac.code.Flags.ENUM; -import static java.util.Comparator.comparing; -import static java.util.Comparator.naturalOrder; import static java.util.Objects.requireNonNull; import static tech.picnic.errorprone.utils.Documentation.BUG_PATTERNS_BASE_URL; import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; -import com.google.common.collect.Comparators; import com.google.common.collect.ImmutableList; import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; @@ -36,12 +32,9 @@ import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCVariableDecl; import com.sun.tools.javac.util.Position; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; import java.util.Set; import javax.lang.model.element.Modifier; -import tech.picnic.errorprone.utils.MoreASTHelpers; /** * A {@link BugChecker} that flags classes with a non-canonical member order. @@ -97,24 +90,41 @@ public Description matchClass(ClassTree tree, VisitorState state) { return Description.NO_MATCH; } - ImmutableList members = getAllTypeMembers(tree, state); - ImmutableList preferredOrdinals = extractPreferredOrdinals(members); + ImmutableList members = getAllTypeMembers(tree, bodyStartPos, state); + ImmutableList sorted = + members.stream() + .filter(e -> e.preferredOrdinal().isPresent()) + .sorted() + .collect(toImmutableList()); - if (Comparators.isInOrder(preferredOrdinals, naturalOrder())) { + if (members.stream() + .filter(e -> e.preferredOrdinal().isPresent()) + .collect(toImmutableList()) + .equals(sorted)) { return Description.NO_MATCH; } - return describeMatch(tree, sortTypeMembers(bodyStartPos, members, state)); + return describeMatch(tree, sortTypeMembers(members, state)); } /** Returns all members that can be moved or may lay between movable ones. */ - // XXX: Check if does have source code. - private ImmutableList getAllTypeMembers(ClassTree tree, VisitorState state) { - return tree.getMembers().stream() - .filter( - member -> !MoreASTHelpers.isGeneratedConstructor(member) && !isEnumDefinition(member)) - .map(m -> new AutoValue_TypeMemberOrder_TypeMember(m, getMemberTypeOrdinal(m, state))) - .collect(toImmutableList()); + private ImmutableList getAllTypeMembers( + ClassTree tree, int bodyStartPos, VisitorState state) { + ImmutableList.Builder builder = ImmutableList.builder(); + @Var int startPos = bodyStartPos; + for (Tree member : tree.getMembers()) { + if (isEnumDefinition(member) || state.getEndPosition(member) == Position.NOPOS) { + continue; + } + + AutoValue_TypeMemberOrder_TypeMember hoi = + new AutoValue_TypeMemberOrder_TypeMember( + member, startPos, state.getEndPosition(member), getMemberTypeOrdinal(member, state)); + builder.add(hoi); + + startPos = state.getEndPosition(member); + } + return builder.build(); } /** @@ -123,7 +133,7 @@ private ImmutableList getAllTypeMembers(ClassTree tree, VisitorState */ private Optional getMemberTypeOrdinal(Tree tree, VisitorState state) { // Check hier ook die andere. - if (isSuppressed(tree, state)) { + if (isSuppressed(tree, state) || isEnumDefinition(tree)) { return Optional.empty(); } return switch (tree.getKind()) { @@ -173,18 +183,6 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { .orElse(Position.NOPOS); } - /** - * Returns a list of the sortable members' preferred ordinals, ordered by the member's position in - * the original source. - */ - private static ImmutableList extractPreferredOrdinals( - ImmutableList members) { - return members.stream() - .filter(m -> m.preferredOrdinal().isPresent()) - .map(m -> m.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ )) - .collect(toImmutableList()); - } - /** * Returns true if {@link Tree} is an enum or an enumerator definition, false otherwise. * @@ -206,34 +204,11 @@ private static boolean isEnumDefinition(Tree tree) { * resolve this. */ private static SuggestedFix sortTypeMembers( - int bodyStartPos, ImmutableList members, VisitorState state) { - List membersWithSource = new ArrayList<>(); - - @Var int start = bodyStartPos; - for (TypeMember member : members) { - int end = state.getEndPosition(member.tree()); - verify( - end != Position.NOPOS && start < end, - "Unexpected member end position, member: %s, end position: %s", - member, - end); - if (member.preferredOrdinal().isPresent()) { - // XXX: .isPresent(). Boven in ook doen, map naar optional. Filter isEmpty, mapNotNull? - membersWithSource.add( - new AutoValue_TypeMemberOrder_MovableTypeMember( - member.tree(), - start, - end, - member.preferredOrdinal().orElseThrow(/* Unreachable due to preceding check. */ ))); - } - start = end; - } - + ImmutableList members, VisitorState state) { CharSequence sourceCode = requireNonNull(state.getSourceCode(), "Source code"); return Streams.zip( - membersWithSource.stream(), - membersWithSource.stream() - .sorted(comparing(MovableTypeMember::preferredOrdinal, naturalOrder())), + members.stream().filter(e -> e.preferredOrdinal().isPresent()), + members.stream().filter(e -> e.preferredOrdinal().isPresent()).sorted(), (original, replacement) -> original.replaceWith(replacement, sourceCode)) .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) .build(); @@ -252,26 +227,32 @@ private static boolean isConstructor(MethodTree methodTree) { return ASTHelpers.getSymbol(methodTree).isConstructor(); } + // /** Type members that have a sourcecode and are not generated, are considered to be movable. + // */ /** XXX: Write this. Every member that is in a ClassTree? */ @AutoValue - abstract static class TypeMember { - abstract Tree tree(); - - abstract Optional preferredOrdinal(); - } - - /** Type members that have a sourcecode and are not generated, are considered to be movable. */ - @AutoValue - abstract static class MovableTypeMember { + abstract static class TypeMember implements Comparable { abstract Tree tree(); abstract int startPosition(); abstract int endPosition(); - abstract int preferredOrdinal(); + abstract Optional preferredOrdinal(); + + @Override + public int compareTo(TypeMemberOrder.TypeMember o) { + if (preferredOrdinal().isEmpty() || o.preferredOrdinal().isEmpty()) { + return 0; + // } else if (preferredOrdinal().isEmpty() && o.preferredOrdinal().isPresent()) { + // return -1; + // } else if (preferredOrdinal().isPresent() && o.preferredOrdinal().isEmpty()) { + // return 1; + } + return preferredOrdinal().orElseThrow().compareTo(o.preferredOrdinal().orElseThrow()); + } - SuggestedFix replaceWith(MovableTypeMember other, CharSequence fullSourceCode) { + SuggestedFix replaceWith(TypeMember other, CharSequence fullSourceCode) { return equals(other) ? SuggestedFix.emptyFix() : SuggestedFix.replace( diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java index e72ae93827..99e0350cd5 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java @@ -207,6 +207,36 @@ void replacementUnmovableMembers() { .doTest(TestMode.TEXT_MATCH); } + @Test + void nieuwe() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + "enum A {", + " FOO;", + "", + " enum InnerEnum {}", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " final int baz = 2;", + "", + " static final int BAR = 1;", + "}") + .addOutputLines( + "A.java", + "enum A {", + " FOO;", + "", + " static final int BAR = 1;", + "", + " @SuppressWarnings(\"TypeMemberOrder\")", + " final int baz = 2;", + "", + " enum InnerEnum {}", + "}") + .doTest(TestMode.TEXT_MATCH); + } + @Test void replacementHandlesGeneratedDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) From 167e3216a989d829fba00d2d304d3706320ab154 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 30 May 2024 20:11:44 +0200 Subject: [PATCH 45/51] Final suggestions, but three failing tests --- .../bugpatterns/TypeMemberOrder.java | 54 ++++++++----------- .../bugpatterns/TypeMemberOrderEnumTest.java | 30 ----------- 2 files changed, 21 insertions(+), 63 deletions(-) diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java index 7cc59459f0..1323cc10f9 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java @@ -56,10 +56,6 @@ * {@code DeclarationOrderCheck} */ // XXX: Consider introducing support for ordering members in records or annotation definitions. -// XXX: Merge the type members, go over all of them, check if they have a source code and then don't -// consider them. -// Make sure that we implement comparable and can just call "sort" on the list and they are ready. -// As a result we don't need the optionals anymore. @AutoService(BugChecker.class) @BugPattern( summary = "Type members should be defined in a canonical order", @@ -90,17 +86,11 @@ public Description matchClass(ClassTree tree, VisitorState state) { return Description.NO_MATCH; } - ImmutableList members = getAllTypeMembers(tree, bodyStartPos, state); - ImmutableList sorted = - members.stream() - .filter(e -> e.preferredOrdinal().isPresent()) - .sorted() - .collect(toImmutableList()); + ImmutableList members = + getAllTypeMembers(tree, bodyStartPos, state).stream().collect(toImmutableList()); + ImmutableList sorted = members.stream().sorted().collect(toImmutableList()); - if (members.stream() - .filter(e -> e.preferredOrdinal().isPresent()) - .collect(toImmutableList()) - .equals(sorted)) { + if (members.equals(sorted)) { return Description.NO_MATCH; } @@ -111,18 +101,21 @@ public Description matchClass(ClassTree tree, VisitorState state) { private ImmutableList getAllTypeMembers( ClassTree tree, int bodyStartPos, VisitorState state) { ImmutableList.Builder builder = ImmutableList.builder(); - @Var int startPos = bodyStartPos; + @Var int currentStartPos = bodyStartPos; for (Tree member : tree.getMembers()) { - if (isEnumDefinition(member) || state.getEndPosition(member) == Position.NOPOS) { + if (state.getEndPosition(member) == Position.NOPOS) { continue; } - AutoValue_TypeMemberOrder_TypeMember hoi = - new AutoValue_TypeMemberOrder_TypeMember( - member, startPos, state.getEndPosition(member), getMemberTypeOrdinal(member, state)); - builder.add(hoi); + int treeStartPos = currentStartPos; + getMemberTypeOrdinal(member, state) + .ifPresent( + e -> + builder.add( + new AutoValue_TypeMemberOrder_TypeMember( + member, treeStartPos, state.getEndPosition(member), e))); - startPos = state.getEndPosition(member); + currentStartPos = state.getEndPosition(member); } return builder.build(); } @@ -132,7 +125,6 @@ private ImmutableList getAllTypeMembers( * including it lacking a preferred ordinal. */ private Optional getMemberTypeOrdinal(Tree tree, VisitorState state) { - // Check hier ook die andere. if (isSuppressed(tree, state) || isEnumDefinition(tree)) { return Optional.empty(); } @@ -207,8 +199,8 @@ private static SuggestedFix sortTypeMembers( ImmutableList members, VisitorState state) { CharSequence sourceCode = requireNonNull(state.getSourceCode(), "Source code"); return Streams.zip( - members.stream().filter(e -> e.preferredOrdinal().isPresent()), - members.stream().filter(e -> e.preferredOrdinal().isPresent()).sorted(), + members.stream(), + members.stream().sorted(), (original, replacement) -> original.replaceWith(replacement, sourceCode)) .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) .build(); @@ -238,18 +230,14 @@ abstract static class TypeMember implements Comparable { abstract int endPosition(); - abstract Optional preferredOrdinal(); + abstract Integer preferredOrdinal(); @Override public int compareTo(TypeMemberOrder.TypeMember o) { - if (preferredOrdinal().isEmpty() || o.preferredOrdinal().isEmpty()) { - return 0; - // } else if (preferredOrdinal().isEmpty() && o.preferredOrdinal().isPresent()) { - // return -1; - // } else if (preferredOrdinal().isPresent() && o.preferredOrdinal().isEmpty()) { - // return 1; - } - return preferredOrdinal().orElseThrow().compareTo(o.preferredOrdinal().orElseThrow()); + // if (preferredOrdinal().isEmpty() || o.preferredOrdinal().isEmpty()) { + // return 0; + // } + return preferredOrdinal().compareTo(o.preferredOrdinal()); } SuggestedFix replaceWith(TypeMember other, CharSequence fullSourceCode) { diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java index 99e0350cd5..e72ae93827 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java @@ -207,36 +207,6 @@ void replacementUnmovableMembers() { .doTest(TestMode.TEXT_MATCH); } - @Test - void nieuwe() { - BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) - .addInputLines( - "A.java", - "enum A {", - " FOO;", - "", - " enum InnerEnum {}", - "", - " @SuppressWarnings(\"TypeMemberOrder\")", - " final int baz = 2;", - "", - " static final int BAR = 1;", - "}") - .addOutputLines( - "A.java", - "enum A {", - " FOO;", - "", - " static final int BAR = 1;", - "", - " @SuppressWarnings(\"TypeMemberOrder\")", - " final int baz = 2;", - "", - " enum InnerEnum {}", - "}") - .doTest(TestMode.TEXT_MATCH); - } - @Test void replacementHandlesGeneratedDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) From 1306bc0ef8e6597fbc1907e9ef3bed3e7e40563e Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 2 Jun 2024 16:02:28 +0200 Subject: [PATCH 46/51] Move `TypeMemberOrder` BugChecker into `experimental` module --- error-prone-experimental/pom.xml | 5 +++++ .../experimental}/bugpatterns/TypeMemberOrder.java | 8 +++----- .../bugpatterns/TypeMemberOrderClassTest.java | 4 +++- .../bugpatterns/TypeMemberOrderEnumTest.java | 6 +++++- .../bugpatterns/TypeMemberOrderInterfaceTest.java | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) rename {error-prone-contrib/src/main/java/tech/picnic/errorprone => error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental}/bugpatterns/TypeMemberOrder.java (96%) rename {error-prone-contrib/src/test/java/tech/picnic/errorprone => error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental}/bugpatterns/TypeMemberOrderClassTest.java (99%) rename {error-prone-contrib/src/test/java/tech/picnic/errorprone => error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental}/bugpatterns/TypeMemberOrderEnumTest.java (98%) rename {error-prone-contrib/src/test/java/tech/picnic/errorprone => error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental}/bugpatterns/TypeMemberOrderInterfaceTest.java (98%) diff --git a/error-prone-experimental/pom.xml b/error-prone-experimental/pom.xml index cfcfb622de..719e27cbb5 100644 --- a/error-prone-experimental/pom.xml +++ b/error-prone-experimental/pom.xml @@ -45,6 +45,11 @@ auto-service-annotations provided + + com.google.auto.value + auto-value-annotations + provided + com.google.guava guava diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java similarity index 96% rename from error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java rename to error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java index 1323cc10f9..8d9b9c7c4f 100644 --- a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrder.java +++ b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java @@ -1,6 +1,5 @@ -package tech.picnic.errorprone.bugpatterns; +package tech.picnic.errorprone.experimental.bugpatterns; -import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.errorprone.BugPattern.LinkType.CUSTOM; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.BugPattern.StandardTags.STYLE; @@ -86,9 +85,8 @@ public Description matchClass(ClassTree tree, VisitorState state) { return Description.NO_MATCH; } - ImmutableList members = - getAllTypeMembers(tree, bodyStartPos, state).stream().collect(toImmutableList()); - ImmutableList sorted = members.stream().sorted().collect(toImmutableList()); + ImmutableList members = getAllTypeMembers(tree, bodyStartPos, state); + ImmutableList sorted = ImmutableList.sortedCopyOf(members); if (members.equals(sorted)) { return Description.NO_MATCH; diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java similarity index 99% rename from error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java rename to error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java index 49f8695562..f0ce4f97cf 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java @@ -1,4 +1,4 @@ -package tech.picnic.errorprone.bugpatterns; +package tech.picnic.errorprone.experimental.bugpatterns; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; @@ -291,6 +291,7 @@ void replacementDanglingComments() { " static {", " System.out.println(\"foo\");", " }", + "", " /* `bar` field's dangling comment */", "", " private final int bar = 2;", @@ -303,6 +304,7 @@ void replacementDanglingComments() { "class A {", " // `foo` field's comment", " private static final int foo = 1;", + "", " /* `bar` field's dangling comment */", "", " private final int bar = 2;", diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java similarity index 98% rename from error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java rename to error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java index e72ae93827..1f3a27acef 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java @@ -1,8 +1,9 @@ -package tech.picnic.errorprone.bugpatterns; +package tech.picnic.errorprone.experimental.bugpatterns; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; final class TypeMemberOrderEnumTest { @@ -85,6 +86,7 @@ void identification() { .doTest(); } + @Disabled @Test void replacement() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) @@ -207,6 +209,7 @@ void replacementUnmovableMembers() { .doTest(TestMode.TEXT_MATCH); } + @Disabled @Test void replacementHandlesGeneratedDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) @@ -261,6 +264,7 @@ void replacementHandlesGeneratedDefaultConstructor() { .doTest(TestMode.TEXT_MATCH); } + @Disabled @Test void replacementDanglingComments() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderInterfaceTest.java similarity index 98% rename from error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java rename to error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderInterfaceTest.java index 5186efc15a..ed648df079 100644 --- a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TypeMemberOrderInterfaceTest.java +++ b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderInterfaceTest.java @@ -1,4 +1,4 @@ -package tech.picnic.errorprone.bugpatterns; +package tech.picnic.errorprone.experimental.bugpatterns; import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; From 59a0616114680e8afeeeaaa37357a7278bf59da7 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sun, 2 Jun 2024 17:00:44 +0200 Subject: [PATCH 47/51] Fix & enable `TypeMemberOrderEnumTest` --- .../errorprone/experimental/bugpatterns/TypeMemberOrder.java | 2 +- .../experimental/bugpatterns/TypeMemberOrderEnumTest.java | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java index 8d9b9c7c4f..3b3933f08b 100644 --- a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java +++ b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java @@ -113,7 +113,7 @@ private ImmutableList getAllTypeMembers( new AutoValue_TypeMemberOrder_TypeMember( member, treeStartPos, state.getEndPosition(member), e))); - currentStartPos = state.getEndPosition(member); + currentStartPos = Math.max(currentStartPos, state.getEndPosition(member)); } return builder.build(); } diff --git a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java index 1f3a27acef..bfdbea270e 100644 --- a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java @@ -3,7 +3,6 @@ import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; import com.google.errorprone.CompilationTestHelper; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; final class TypeMemberOrderEnumTest { @@ -86,7 +85,6 @@ void identification() { .doTest(); } - @Disabled @Test void replacement() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) @@ -209,7 +207,6 @@ void replacementUnmovableMembers() { .doTest(TestMode.TEXT_MATCH); } - @Disabled @Test void replacementHandlesGeneratedDefaultConstructor() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) @@ -264,7 +261,6 @@ void replacementHandlesGeneratedDefaultConstructor() { .doTest(TestMode.TEXT_MATCH); } - @Disabled @Test void replacementDanglingComments() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) From 284a1536858b94befd646c35b8cca40e8dce57a4 Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Thu, 30 May 2024 20:33:58 +0200 Subject: [PATCH 48/51] Cleanup --- .../experimental/bugpatterns/TypeMemberOrder.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java index 3b3933f08b..9fd385aa1c 100644 --- a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java +++ b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java @@ -86,9 +86,9 @@ public Description matchClass(ClassTree tree, VisitorState state) { } ImmutableList members = getAllTypeMembers(tree, bodyStartPos, state); - ImmutableList sorted = ImmutableList.sortedCopyOf(members); + ImmutableList sortedMembers = ImmutableList.sortedCopyOf(members); - if (members.equals(sorted)) { + if (members.equals(sortedMembers)) { return Description.NO_MATCH; } @@ -123,7 +123,7 @@ private ImmutableList getAllTypeMembers( * including it lacking a preferred ordinal. */ private Optional getMemberTypeOrdinal(Tree tree, VisitorState state) { - if (isSuppressed(tree, state) || isEnumDefinition(tree)) { + if (isSuppressed(tree, state) || isEnumeratorDefinition(tree)) { return Optional.empty(); } return switch (tree.getKind()) { @@ -179,7 +179,7 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) * @see com.sun.tools.javac.code.Flags#ENUM */ - private static boolean isEnumDefinition(Tree tree) { + private static boolean isEnumeratorDefinition(Tree tree) { return tree instanceof JCVariableDecl variableDecl && (variableDecl.mods.flags & ENUM) != 0; } @@ -217,9 +217,6 @@ private static boolean isConstructor(MethodTree methodTree) { return ASTHelpers.getSymbol(methodTree).isConstructor(); } - // /** Type members that have a sourcecode and are not generated, are considered to be movable. - // */ - /** XXX: Write this. Every member that is in a ClassTree? */ @AutoValue abstract static class TypeMember implements Comparable { abstract Tree tree(); @@ -232,9 +229,6 @@ abstract static class TypeMember implements Comparable { @Override public int compareTo(TypeMemberOrder.TypeMember o) { - // if (preferredOrdinal().isEmpty() || o.preferredOrdinal().isEmpty()) { - // return 0; - // } return preferredOrdinal().compareTo(o.preferredOrdinal()); } From 070e0652884bff4dc78fa4e5b3922d67a491a9dc Mon Sep 17 00:00:00 2001 From: Rick Ossendrijver Date: Wed, 12 Jun 2024 09:38:54 +0200 Subject: [PATCH 49/51] Pair programming --- .../experimental/bugpatterns/TypeMemberOrder.java | 1 + .../bugpatterns/TypeMemberOrderClassTest.java | 8 +++++++- .../bugpatterns/TypeMemberOrderEnumTest.java | 10 ++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java index 9fd385aa1c..18e5afc6c0 100644 --- a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java +++ b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java @@ -113,6 +113,7 @@ private ImmutableList getAllTypeMembers( new AutoValue_TypeMemberOrder_TypeMember( member, treeStartPos, state.getEndPosition(member), e))); + /* XXX: Write explanation about this enum. */ currentStartPos = Math.max(currentStartPos, state.getEndPosition(member)); } return builder.build(); diff --git a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java index f0ce4f97cf..4828bcee2d 100644 --- a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java @@ -96,7 +96,13 @@ void replacement() { .addInputLines( "A.java", "class A {", - " class Inner {}", + " class Inner {", + " int innerFoo() {", + " return 1;", + " }", + "", + " private final int innerBar = 2;", + " }", "", " int foo() {", " return foo;", diff --git a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java index bfdbea270e..db60778373 100644 --- a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java +++ b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderEnumTest.java @@ -91,7 +91,8 @@ void replacement() { .addInputLines( "A.java", "enum A {", - " FOO;", + " FOO,", + " BAR;", "", " class InnerClass {}", "", @@ -112,13 +113,14 @@ void replacement() { " }", "", " final int baz = 2;", - " static final int BAR = 1;", + " static final int BAZ = 1;", "}") .addOutputLines( "A.java", "enum A {", - " FOO;", - " static final int BAR = 1;", + " FOO,", + " BAR;", + " static final int BAZ = 1;", "", " final int baz = 2;", "", From 47e27ebff6d05eb6a423d0f8a0fd9541fdfc2074 Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Fri, 14 Jun 2024 23:10:48 +0200 Subject: [PATCH 50/51] Merge fixes of inner type decls, enable sorting nested types in one-go --- error-prone-experimental/pom.xml | 12 ++ .../bugpatterns/TypeMemberOrder.java | 105 ++++++++++++++---- .../bugpatterns/TypeMemberOrderClassTest.java | 46 +++++++- ...erOrderNestedCompilationUnitTestInput.java | 65 +++++++++++ ...rOrderNestedCompilationUnitTestOutput.java | 67 +++++++++++ 5 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestInput.java create mode 100644 error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestOutput.java diff --git a/error-prone-experimental/pom.xml b/error-prone-experimental/pom.xml index 719e27cbb5..68730960f9 100644 --- a/error-prone-experimental/pom.xml +++ b/error-prone-experimental/pom.xml @@ -73,4 +73,16 @@ test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + true + + + + diff --git a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java index 18e5afc6c0..84f69e74e7 100644 --- a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java +++ b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java @@ -10,12 +10,14 @@ import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Streams; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.Var; import com.google.errorprone.bugpatterns.BugChecker; -import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; +import com.google.errorprone.fixes.Replacement; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; @@ -23,6 +25,7 @@ import com.google.errorprone.util.ErrorProneTokens; import com.sun.source.tree.BlockTree; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; @@ -55,6 +58,7 @@ * {@code DeclarationOrderCheck} */ // XXX: Consider introducing support for ordering members in records or annotation definitions. +// XXX: Exclude checkstyle from running against (this checker's) test files. @AutoService(BugChecker.class) @BugPattern( summary = "Type members should be defined in a canonical order", @@ -62,17 +66,42 @@ linkType = CUSTOM, severity = WARNING, tags = STYLE) -public final class TypeMemberOrder extends BugChecker implements ClassTreeMatcher { +public final class TypeMemberOrder extends BugChecker implements CompilationUnitTreeMatcher { private static final long serialVersionUID = 1L; /** Instantiates a new {@link TypeMemberOrder} instance. */ public TypeMemberOrder() {} @Override - public Description matchClass(ClassTree tree, VisitorState state) { + public Description matchCompilationUnit( + CompilationUnitTree compilationUnitTree, VisitorState state) { + SuggestedFix.Builder suggestedFixes = SuggestedFix.builder(); + for (Tree tree : compilationUnitTree.getTypeDecls()) { + if (!isSuppressed(tree, state) && tree instanceof ClassTree classTree) { + suggestedFixes.merge( + matchClass(classTree, state, (JCTree.JCCompilationUnit) compilationUnitTree)); + } + } + SuggestedFix suggestedFix = suggestedFixes.build(); + if (suggestedFix.isEmpty()) { + return Description.NO_MATCH; + } + return describeMatch(compilationUnitTree, suggestedFix); + } + + /** + * Matches a class. + * + * @param tree xxx + * @param state xxx + * @param compilationUnit xxx + * @return xxx + */ + public SuggestedFix matchClass( + ClassTree tree, VisitorState state, JCTree.JCCompilationUnit compilationUnit) { Kind treeKind = tree.getKind(); if (treeKind != Kind.CLASS && treeKind != Kind.INTERFACE && treeKind != Kind.ENUM) { - return Description.NO_MATCH; + return SuggestedFix.emptyFix(); } int bodyStartPos = getBodyStartPos(tree, state); @@ -82,17 +111,52 @@ public Description matchClass(ClassTree tree, VisitorState state) { * that (part of) its code was generated. Even if the source code for a subset of its members * is available, dealing with this edge case is not worth the trouble. */ - return Description.NO_MATCH; + return SuggestedFix.emptyFix(); } ImmutableList members = getAllTypeMembers(tree, bodyStartPos, state); - ImmutableList sortedMembers = ImmutableList.sortedCopyOf(members); + boolean topLevelSorted = members.equals(ImmutableList.sortedCopyOf(members)); - if (members.equals(sortedMembers)) { - return Description.NO_MATCH; + if (topLevelSorted) { + SuggestedFix.Builder nestedSuggestedFixes = SuggestedFix.builder(); + for (TypeMember member : members) { + if (member.tree() instanceof ClassTree memberClassTree) { + SuggestedFix other = matchClass(memberClassTree, state, compilationUnit); + nestedSuggestedFixes.merge(other); + } + } + return nestedSuggestedFixes.build(); } - return describeMatch(tree, sortTypeMembers(members, state)); + CharSequence source = requireNonNull(state.getSourceCode(), "Source code"); + ImmutableMap.Builder typeMemberSource = ImmutableMap.builder(); + + for (TypeMember member : members) { + if (member.tree() instanceof ClassTree memberClassTree) { + SuggestedFix suggestedFix = matchClass(memberClassTree, state, compilationUnit); + @Var + String memberSource = + source.subSequence(member.startPosition(), member.endPosition()).toString(); + // Diff between memberSource and replacement positions. + @Var int diff = -member.startPosition(); + for (Replacement replacement : suggestedFix.getReplacements(compilationUnit.endPositions)) { + memberSource = + memberSource.subSequence(0, replacement.startPosition() + diff) + + replacement.replaceWith() + + memberSource.subSequence( + replacement.endPosition() + diff, memberSource.length()); + diff += + replacement.replaceWith().length() + - (replacement.endPosition() - replacement.startPosition()); + } + typeMemberSource.put(member, memberSource); + } else { + typeMemberSource.put( + member, source.subSequence(member.startPosition(), member.endPosition()).toString()); + } + } + + return sortTypeMembers(members, typeMemberSource.build()); } /** Returns all members that can be moved or may lay between movable ones. */ @@ -177,7 +241,6 @@ private static int getBodyStartPos(ClassTree tree, VisitorState state) { /** * Returns true if {@link Tree} is an enum or an enumerator definition, false otherwise. * - * @see com.sun.tools.javac.tree.Pretty#isEnumerator(JCTree) * @see com.sun.tools.javac.code.Flags#ENUM */ private static boolean isEnumeratorDefinition(Tree tree) { @@ -195,12 +258,19 @@ private static boolean isEnumeratorDefinition(Tree tree) { * resolve this. */ private static SuggestedFix sortTypeMembers( - ImmutableList members, VisitorState state) { - CharSequence sourceCode = requireNonNull(state.getSourceCode(), "Source code"); + ImmutableList members, ImmutableMap sourceCode) { return Streams.zip( members.stream(), members.stream().sorted(), - (original, replacement) -> original.replaceWith(replacement, sourceCode)) + (original, replacement) -> { + String replacementSource = requireNonNull(sourceCode.get(replacement), "replacement"); + return original.equals(replacement) + ? SuggestedFix.emptyFix() + : SuggestedFix.replace( + original.startPosition(), + original.endPosition(), + replacementSource.toString()); + }) .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) .build(); } @@ -232,14 +302,5 @@ abstract static class TypeMember implements Comparable { public int compareTo(TypeMemberOrder.TypeMember o) { return preferredOrdinal().compareTo(o.preferredOrdinal()); } - - SuggestedFix replaceWith(TypeMember other, CharSequence fullSourceCode) { - return equals(other) - ? SuggestedFix.emptyFix() - : SuggestedFix.replace( - startPosition(), - endPosition(), - fullSourceCode.subSequence(other.startPosition(), other.endPosition()).toString()); - } } } diff --git a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java index 4828bcee2d..f1b50aaf91 100644 --- a/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java +++ b/error-prone-experimental/src/test/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrderClassTest.java @@ -1,8 +1,12 @@ package tech.picnic.errorprone.experimental.bugpatterns; +import static java.nio.charset.StandardCharsets.UTF_8; + import com.google.errorprone.BugCheckerRefactoringTestHelper; import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; import com.google.errorprone.CompilationTestHelper; +import java.io.BufferedReader; +import java.io.InputStreamReader; import org.junit.jupiter.api.Test; final class TypeMemberOrderClassTest { @@ -142,11 +146,47 @@ void replacement() { " return foo;", " }", "", - " class Inner {}", + " class Inner {", + "", + " private final int innerBar = 2;", + "", + " int innerFoo() {", + " return 1;", + " }", + " }", "}") .doTest(TestMode.TEXT_MATCH); } + @Test + void replacementNestedClasses() { + BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) + .addInputLines( + "A.java", + // XXX: Use dedicated helper method to read test files here and below. + new BufferedReader( + new InputStreamReader( + this.getClass() + .getClassLoader() + .getResourceAsStream( + "TypeMemberOrderNestedCompilationUnitTestInput.java"), + UTF_8)) + .lines() + .toArray(String[]::new)) + .addOutputLines( + "A.java", + new BufferedReader( + new InputStreamReader( + this.getClass() + .getClassLoader() + .getResourceAsStream( + "TypeMemberOrderNestedCompilationUnitTestOutput.java"), + UTF_8)) + .lines() + .toArray(String[]::new)) + .doTest(TestMode.TEXT_MATCH); + } + @Test void replacementAbstractMethods() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) @@ -345,7 +385,7 @@ void replacementComplexAnnotation() { BugCheckerRefactoringTestHelper.newInstance(TypeMemberOrder.class, getClass()) .addInputLines( "A.java", - "final class A {", + "class A {", "", " @interface AnnotationWithClassReferences {", " Class[] value() default {};", @@ -365,7 +405,7 @@ void replacementComplexAnnotation() { "}") .addOutputLines( "A.java", - "final class A {", + "class A {", "", " @interface AnnotationWithClassReferences {", " Class[] value() default {};", diff --git a/error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestInput.java b/error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestInput.java new file mode 100644 index 0000000000..52d2b6971b --- /dev/null +++ b/error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestInput.java @@ -0,0 +1,65 @@ +class A { + class AInner2 { + private static final int foo = 1; + int bar = 2; + + class AInner2Inner1 { + private static final int foo = 1; + int bar = 2; + } + + class AInner2Inner2 { + int bar = 2; + private static final int foo = 1; + } + } + + private static final int foo = 1; + + class OnlyTopLevelSortedClass { + private static final int foo = 1; + int bar = 2; + + class UnsortedNestedClass { + int bar = 2; + private static final int foo = 1; + } + } + + class AInner1 { + class AInner1Inner1 { + int bar = 2; + private static final int foo = 1; + } + + enum DeeplyNestedEnum { + /** FOO's JavaDoc */ + FOO, + /* Dangling comment trailing enumerations. */ ; + + /** `quz` method's dangling comment */ + ; + + /** `quz` method's comment */ + void qux() {} + + // `baz` method's comment + final int baz = 2; + + static final int BAR = 1; + // trailing comment + } + + private static final int foo = 1; + + class AInner1Inner2 { + int bar = 2; + private static final int foo = 1; + } + + int bar = 2; + } + + static int baz = 3; + private final int bar = 2; +} diff --git a/error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestOutput.java b/error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestOutput.java new file mode 100644 index 0000000000..445b76220e --- /dev/null +++ b/error-prone-experimental/src/test/resources/TypeMemberOrderNestedCompilationUnitTestOutput.java @@ -0,0 +1,67 @@ +class A { + + private static final int foo = 1; + + static int baz = 3; + private final int bar = 2; + + class AInner2 { + private static final int foo = 1; + int bar = 2; + + class AInner2Inner1 { + private static final int foo = 1; + int bar = 2; + } + + class AInner2Inner2 { + private static final int foo = 1; + int bar = 2; + } + } + + class OnlyTopLevelSortedClass { + private static final int foo = 1; + int bar = 2; + + class UnsortedNestedClass { + private static final int foo = 1; + int bar = 2; + } + } + + class AInner1 { + + private static final int foo = 1; + + int bar = 2; + + class AInner1Inner1 { + private static final int foo = 1; + int bar = 2; + } + + enum DeeplyNestedEnum { + /** FOO's JavaDoc */ + FOO, + /* Dangling comment trailing enumerations. */ ; + + static final int BAR = 1; + + // `baz` method's comment + final int baz = 2; + + /** `quz` method's dangling comment */ + ; + + /** `quz` method's comment */ + void qux() {} + // trailing comment + } + + class AInner1Inner2 { + private static final int foo = 1; + int bar = 2; + } + } +} From 0bc5f94330d9049e23d9582f866a3e3efb8aa23b Mon Sep 17 00:00:00 2001 From: Benedek Halasi Date: Sat, 7 Dec 2024 08:20:03 +0100 Subject: [PATCH 51/51] fixup! Merge fixes of inner type decls, enable sorting nested types in one-go --- .../bugpatterns/TypeMemberOrder.java | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java index 84f69e74e7..76e4f722ba 100644 --- a/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java +++ b/error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java @@ -89,15 +89,7 @@ public Description matchCompilationUnit( return describeMatch(compilationUnitTree, suggestedFix); } - /** - * Matches a class. - * - * @param tree xxx - * @param state xxx - * @param compilationUnit xxx - * @return xxx - */ - public SuggestedFix matchClass( + private SuggestedFix matchClass( ClassTree tree, VisitorState state, JCTree.JCCompilationUnit compilationUnit) { Kind treeKind = tree.getKind(); if (treeKind != Kind.CLASS && treeKind != Kind.INTERFACE && treeKind != Kind.ENUM) { @@ -267,9 +259,7 @@ private static SuggestedFix sortTypeMembers( return original.equals(replacement) ? SuggestedFix.emptyFix() : SuggestedFix.replace( - original.startPosition(), - original.endPosition(), - replacementSource.toString()); + original.startPosition(), original.endPosition(), replacementSource); }) .reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge) .build(); @@ -296,11 +286,11 @@ abstract static class TypeMember implements Comparable { abstract int endPosition(); - abstract Integer preferredOrdinal(); + abstract int preferredOrdinal(); @Override public int compareTo(TypeMemberOrder.TypeMember o) { - return preferredOrdinal().compareTo(o.preferredOrdinal()); + return Integer.compare(preferredOrdinal(), o.preferredOrdinal()); } } }