Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce TypeMemberOrder bug checker #636

Open
wants to merge 51 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
4487ec0
Naive impl
benhalasi May 3, 2023
07c19c8
Consider comments
benhalasi May 16, 2023
f962899
Prepare the code for others to see
benhalasi May 17, 2023
4140da3
Test if error message contains crucial information
benhalasi May 18, 2023
5b522ae
Code clean-up, formatting, docs, TEMPORARY supress warnings
benhalasi May 18, 2023
d887fdc
Cannot verify whitespace related requirements as `TestMode.TEXT_MATCH…
benhalasi May 18, 2023
1330a2f
Apply my suggestions
benhalasi May 18, 2023
fb76a55
Suggestions
rickie May 25, 2023
2e29731
Drop `replaceIncludingComments`
rickie May 25, 2023
d10a9ed
Revert "Drop `replaceIncludingComments`"
rickie May 25, 2023
7a5e504
Suggestions
benhalasi Jul 9, 2023
dc79300
Checkstyle
benhalasi Jul 10, 2023
76ee961
suggestions
oxkitsune Jul 24, 2023
62823ff
Remove stray unused import
oxkitsune Jul 24, 2023
7fcb987
Simplifing logic, killing mutants
benhalasi Jul 25, 2023
d01a959
"Fix" previous token detection
benhalasi Jul 28, 2023
c12e2e0
Assume that only generated members' end position is unavailable.
benhalasi Jul 28, 2023
381b063
Suggestions on naming, minor organizational changes, rewordings
benhalasi Jul 29, 2023
a47b718
Reproduce error-prone token start - end position overlap
benhalasi Jul 29, 2023
156c2ba
Suggestions
Stephan202 Aug 12, 2023
197eeba
List all remarks as TODOs, or address them
benhalasi Jan 14, 2024
4bbb603
Rename `*class*` to `*type*`
benhalasi Jan 14, 2024
909449d
Add identification test for an empty class, sync identifaction test w…
benhalasi Jan 14, 2024
1a306a6
Verify that SupressWarnings("all" or "TypeMemberOrdering") are not or…
benhalasi Jan 14, 2024
4d772d6
Suggestions
benhalasi Jan 14, 2024
819596b
Test initializer block ordering
benhalasi Jan 14, 2024
9d436b9
Implement initializer block ordering
benhalasi Jan 14, 2024
b922cf0
Test inner type ordering
benhalasi Jan 14, 2024
ed5efe3
Implement inner type ordering
benhalasi Jan 14, 2024
f63971e
Test not-ordering unordered members annotated w/ SuppressWarnings
benhalasi Jan 14, 2024
d93e8a1
Stop sorting unsorted members annotated w/ SuppressWarnings - all or …
benhalasi Jan 14, 2024
738dcf2
Rename `TypeMemberWithComments` to `TypeMember`
benhalasi Jan 14, 2024
e5d1aa2
Support classes, interfaces and enums
benhalasi Jan 21, 2024
4e33c52
Post rebase fix
rickie Feb 22, 2024
fb14933
Pair programming session
rickie Feb 22, 2024
d444106
Test abstract and default methods
benhalasi Mar 3, 2024
ecd91d1
Tweaks
benhalasi Mar 4, 2024
00bd247
Suggestions
rickie Mar 23, 2024
8f171a5
Improve more tests
rickie Apr 8, 2024
d0b9479
Reproduce syntactically wrong replacement with annotations containing…
benhalasi May 4, 2024
5652cb6
Fix syntactically wrong replecement with annotations containing `LBRACE`
benhalasi May 4, 2024
173aebf
Suggestions
rickie May 27, 2024
2ae6f20
Suggestions part 1
rickie May 30, 2024
c521778
Try to implement new thing
rickie May 30, 2024
167e321
Final suggestions, but three failing tests
rickie May 30, 2024
1306bc0
Move `TypeMemberOrder` BugChecker into `experimental` module
benhalasi Jun 2, 2024
59a0616
Fix & enable `TypeMemberOrderEnumTest`
benhalasi Jun 2, 2024
284a153
Cleanup
rickie May 30, 2024
070e065
Pair programming
rickie Jun 12, 2024
47e27eb
Merge fixes of inner type decls, enable sorting nested types in one-go
benhalasi Jun 14, 2024
0bc5f94
fixup! Merge fixes of inner type decls, enable sorting nested types i…
benhalasi Dec 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions error-prone-experimental/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
<artifactId>auto-service-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
Expand All @@ -68,4 +73,16 @@
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
package tech.picnic.errorprone.experimental.bugpatterns;

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.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.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.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;
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.CompilationUnitTree;
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.Optional;
import java.util.Set;
import javax.lang.model.element.Modifier;

/**
* A {@link BugChecker} that flags classes with a non-canonical member order.
*
* <p>Class members should be ordered as follows:
*
* <ol>
* <li>Static fields
* <li>Instance fields
* <li>Static initializer blocks
* <li>Instance initializer blocks
* <li>Constructors
* <li>Methods
* <li>Nested classes, interfaces and enums
* </ol>
*
* @see <a
* href="https://checkstyle.sourceforge.io/apidocs/com/puppycrawl/tools/checkstyle/checks/coding/DeclarationOrderCheck.html">Checkstyle's
* {@code DeclarationOrderCheck}</a>
*/
// 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",
link = BUG_PATTERNS_BASE_URL + "TypeMemberOrder",
linkType = CUSTOM,
severity = WARNING,
tags = STYLE)
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 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);
}

private SuggestedFix matchClass(
ClassTree tree, VisitorState state, JCTree.JCCompilationUnit compilationUnit) {
Kind treeKind = tree.getKind();
if (treeKind != Kind.CLASS && treeKind != Kind.INTERFACE && treeKind != Kind.ENUM) {

Check warning on line 95 in error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java

View workflow job for this annotation

GitHub Actions / pitest

3 different changes can be made to line 95 without causing a test to fail

removed conditional - replaced equality check with false (covered by 18 tests RemoveConditionalMutator_EQUAL_ELSE) removed conditional - replaced equality check with false (covered by 12 tests RemoveConditionalMutator_EQUAL_ELSE) removed conditional - replaced equality check with false (covered by 10 tests RemoveConditionalMutator_EQUAL_ELSE)
return SuggestedFix.emptyFix();

Check warning on line 96 in error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 96 without causing a test to fail

replaced return value with null for matchClass (no tests cover this line NullReturnValsMutator)
}

int bodyStartPos = getBodyStartPos(tree, state);
if (bodyStartPos == Position.NOPOS) {

Check warning on line 100 in error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 100 without causing a test to fail

removed conditional - replaced equality check with false (covered by 18 tests RemoveConditionalMutator_EQUAL_ELSE)
/*
* 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 SuggestedFix.emptyFix();
}

ImmutableList<TypeMember> members = getAllTypeMembers(tree, bodyStartPos, state);
boolean topLevelSorted = members.equals(ImmutableList.sortedCopyOf(members));

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();
}

CharSequence source = requireNonNull(state.getSourceCode(), "Source code");
ImmutableMap.Builder<TypeMember, String> 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. */
private ImmutableList<TypeMember> getAllTypeMembers(
ClassTree tree, int bodyStartPos, VisitorState state) {
ImmutableList.Builder<TypeMember> builder = ImmutableList.builder();
@Var int currentStartPos = bodyStartPos;
for (Tree member : tree.getMembers()) {
if (state.getEndPosition(member) == Position.NOPOS) {
continue;
}

int treeStartPos = currentStartPos;
getMemberTypeOrdinal(member, state)
.ifPresent(
e ->
builder.add(
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();
}

/**
* 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<Integer> getMemberTypeOrdinal(Tree tree, VisitorState state) {
if (isSuppressed(tree, state) || isEnumeratorDefinition(tree)) {
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 -> Optional.empty();
};
}

/**
* 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();
/*
* 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) {

Check warning on line 207 in error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java

View workflow job for this annotation

GitHub Actions / pitest

3 different changes can be made to line 207 without causing a test to fail

removed conditional - replaced equality check with false (covered by 18 tests RemoveConditionalMutator_EQUAL_ELSE) removed conditional - replaced equality check with true (covered by 18 tests RemoveConditionalMutator_EQUAL_IF) removed conditional - replaced equality check with true (covered by 18 tests RemoveConditionalMutator_EQUAL_IF)
return Position.NOPOS;

Check warning on line 208 in error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to line 208 without causing a test to fail

replaced int return with 0 for getBodyStartPos (no tests cover this line PrimitiveReturnsMutator)
}

/*
* 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.
*/
.dropWhile(token -> tree.getKind() == Kind.ENUM && token.kind() != TokenKind.SEMI)
.findFirst()
.map(ErrorProneToken::endPos)
.orElse(Position.NOPOS);
}

/**
* Returns true if {@link Tree} is an enum or an enumerator definition, false otherwise.
*
* @see com.sun.tools.javac.code.Flags#ENUM
*/
private static boolean isEnumeratorDefinition(Tree tree) {
return tree instanceof JCVariableDecl variableDecl && (variableDecl.mods.flags & ENUM) != 0;
}

/**
* 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(
ImmutableList<TypeMember> members, ImmutableMap<TypeMember, String> sourceCode) {
return Streams.zip(
members.stream(),
members.stream().sorted(),
(original, replacement) -> {
String replacementSource = requireNonNull(sourceCode.get(replacement), "replacement");
return original.equals(replacement)

Check warning on line 259 in error-prone-experimental/src/main/java/tech/picnic/errorprone/experimental/bugpatterns/TypeMemberOrder.java

View workflow job for this annotation

GitHub Actions / pitest

A change can be made to a lambda on line 259 without causing a test to fail

removed conditional - replaced equality check with false in 1st lambda in sortTypeMembers (covered by 18 tests RemoveConditionalMutator_EQUAL_ELSE)
? SuggestedFix.emptyFix()
: SuggestedFix.replace(
original.startPosition(), original.endPosition(), replacementSource);
})
.reduce(SuggestedFix.builder(), SuggestedFix.Builder::merge, SuggestedFix.Builder::merge)
.build();
}

private static boolean isStatic(VariableTree variableTree) {
Set<Modifier> 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 implements Comparable<TypeMember> {
abstract Tree tree();

abstract int startPosition();

abstract int endPosition();

abstract int preferredOrdinal();

@Override
public int compareTo(TypeMemberOrder.TypeMember o) {
return Integer.compare(preferredOrdinal(), o.preferredOrdinal());
}
}
}
Loading