-
Notifications
You must be signed in to change notification settings - Fork 39
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 NonStaticImport
check
#450
Changes from all commits
7a1b663
3ba774f
9ea8511
2776580
ac84c26
2e24be8
b6c10ea
22578c2
3f8825b
34e9f07
0d3a329
28e7bec
98c3bf5
cd134d6
81aae74
f03925c
bad3b60
e103c3c
a70444b
6fea4d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
package tech.picnic.errorprone.bugpatterns; | ||
|
||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM; | ||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; | ||
import static com.google.errorprone.BugPattern.StandardTags.STYLE; | ||
import static tech.picnic.errorprone.bugpatterns.StaticImport.STATIC_IMPORT_CANDIDATE_MEMBERS; | ||
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.annotations.VisibleForTesting; | ||
import com.google.common.collect.ImmutableSet; | ||
import com.google.common.collect.ImmutableSetMultimap; | ||
import com.google.common.collect.ImmutableTable; | ||
import com.google.errorprone.BugPattern; | ||
import com.google.errorprone.VisitorState; | ||
import com.google.errorprone.bugpatterns.BugChecker; | ||
import com.google.errorprone.bugpatterns.BugChecker.CompilationUnitTreeMatcher; | ||
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.CompilationUnitTree; | ||
import com.sun.source.tree.IdentifierTree; | ||
import com.sun.source.tree.ImportTree; | ||
import com.sun.source.tree.MemberSelectTree; | ||
import com.sun.source.tree.Tree; | ||
import com.sun.source.util.TreeScanner; | ||
import com.sun.tools.javac.code.Symbol; | ||
import org.jspecify.annotations.Nullable; | ||
import tech.picnic.errorprone.bugpatterns.util.SourceCode; | ||
|
||
/** | ||
* A {@link BugChecker} that flags static imports of type members that should *not* be statically | ||
* imported. | ||
*/ | ||
// XXX: This check is closely linked to `StaticImport`. Consider merging the two. | ||
// XXX: Add suppression support. If qualification of one more more identifiers is suppressed, then | ||
// the associated static import should *not* be removed. | ||
// XXX: Also introduce logic that disallows statically importing `ZoneOffset.ofHours` and other | ||
// `ofXXX`-style methods. | ||
@AutoService(BugChecker.class) | ||
@BugPattern( | ||
summary = "Member should not be statically imported", | ||
link = BUG_PATTERNS_BASE_URL + "NonStaticImport", | ||
linkType = CUSTOM, | ||
severity = SUGGESTION, | ||
tags = STYLE) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned, let's also apply this to |
||
public final class NonStaticImport extends BugChecker implements CompilationUnitTreeMatcher { | ||
private static final long serialVersionUID = 1L; | ||
|
||
/** | ||
* Types whose members should not be statically imported, unless exempted by {@link | ||
* StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS}. | ||
* | ||
* <p>Types listed here should be mutually exclusive with {@link | ||
* StaticImport#STATIC_IMPORT_CANDIDATE_TYPES}. | ||
*/ | ||
@VisibleForTesting | ||
static final ImmutableSet<String> NON_STATIC_IMPORT_CANDIDATE_TYPES = | ||
ImmutableSet.of("com.google.common.base.Strings", "java.time.Clock", "java.time.ZoneOffset"); | ||
|
||
/** | ||
* Type members that should never be statically imported. | ||
* | ||
* <p>Please note that: | ||
* | ||
* <ul> | ||
* <li>Types listed by {@link #NON_STATIC_IMPORT_CANDIDATE_TYPES} and members listed by {@link | ||
* #NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS} should be omitted from this collection. | ||
* <li>This collection should be mutually exclusive with {@link | ||
* StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS}. | ||
* </ul> | ||
*/ | ||
// XXX: Perhaps the set of exempted `java.util.Collections` methods is too strict. For now any | ||
// method name that could be considered "too vague" or could conceivably mean something else in a | ||
// specific context is left out. | ||
static final ImmutableSetMultimap<String, String> NON_STATIC_IMPORT_CANDIDATE_MEMBERS = | ||
ImmutableSetMultimap.<String, String>builder() | ||
.put("com.google.common.base.Predicates", "contains") | ||
.putAll( | ||
"java.util.Collections", | ||
"addAll", | ||
"copy", | ||
"fill", | ||
"list", | ||
"max", | ||
"min", | ||
"nCopies", | ||
"rotate", | ||
"sort", | ||
"swap") | ||
.put("java.util.Locale", "ROOT") | ||
.putAll("java.util.regex.Pattern", "compile", "matches", "quote") | ||
.put("org.springframework.http.MediaType", "ALL") | ||
.build(); | ||
|
||
/** | ||
* Identifiers that should never be statically imported. | ||
* | ||
* <p>Please note that: | ||
* | ||
* <ul> | ||
* <li>Identifiers listed by {@link StaticImport#STATIC_IMPORT_CANDIDATE_MEMBERS} should be | ||
* mutually exclusive with identifiers listed here. | ||
* <li>This list should contain a superset of the identifiers flagged by {@link | ||
* com.google.errorprone.bugpatterns.BadImport}. | ||
* </ul> | ||
*/ | ||
static final ImmutableSet<String> NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS = | ||
ImmutableSet.of( | ||
"builder", | ||
"copyOf", | ||
"create", | ||
"empty", | ||
"from", | ||
"getDefaultInstance", | ||
"INSTANCE", | ||
"MAX", | ||
"MAX_VALUE", | ||
"MIN", | ||
"MIN_VALUE", | ||
"newBuilder", | ||
"newInstance", | ||
"of", | ||
"ONE", | ||
"parse", | ||
"valueOf", | ||
"ZERO"); | ||
|
||
/** Instantiates a new {@link NonStaticImport} instance. */ | ||
public NonStaticImport() {} | ||
|
||
@Override | ||
public Description matchCompilationUnit(CompilationUnitTree tree, VisitorState state) { | ||
ImmutableTable<String, String, UndesiredStaticImport> undesiredStaticImports = | ||
getUndesiredStaticImports(tree, state); | ||
|
||
if (!undesiredStaticImports.isEmpty()) { | ||
Check warning on line 139 in error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/NonStaticImport.java GitHub Actions / pitestA change can be made to line 139 without causing a test to fail
|
||
replaceUndesiredStaticImportUsages(tree, undesiredStaticImports, state); | ||
|
||
for (UndesiredStaticImport staticImport : undesiredStaticImports.values()) { | ||
state.reportMatch( | ||
describeMatch(staticImport.importTree(), staticImport.fixBuilder().build())); | ||
} | ||
} | ||
|
||
/* Any violations have been flagged against the offending static import statement. */ | ||
return Description.NO_MATCH; | ||
} | ||
|
||
private static ImmutableTable<String, String, UndesiredStaticImport> getUndesiredStaticImports( | ||
CompilationUnitTree tree, VisitorState state) { | ||
ImmutableTable.Builder<String, String, UndesiredStaticImport> imports = | ||
ImmutableTable.builder(); | ||
for (ImportTree importTree : tree.getImports()) { | ||
Tree qualifiedIdentifier = importTree.getQualifiedIdentifier(); | ||
if (importTree.isStatic() && qualifiedIdentifier instanceof MemberSelectTree) { | ||
MemberSelectTree memberSelectTree = (MemberSelectTree) qualifiedIdentifier; | ||
String type = SourceCode.treeToString(memberSelectTree.getExpression(), state); | ||
String member = memberSelectTree.getIdentifier().toString(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will revert. |
||
if (shouldNotBeStaticallyImported(type, member)) { | ||
imports.put( | ||
type, | ||
member, | ||
new AutoValue_NonStaticImport_UndesiredStaticImport( | ||
importTree, SuggestedFix.builder().removeStaticImport(type + '.' + member))); | ||
} | ||
} | ||
} | ||
|
||
return imports.build(); | ||
} | ||
|
||
private static boolean shouldNotBeStaticallyImported(String type, String member) { | ||
return (NON_STATIC_IMPORT_CANDIDATE_TYPES.contains(type) | ||
&& !STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type, member)) | ||
|| NON_STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type, member) | ||
|| NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS.contains(member); | ||
} | ||
|
||
private static void replaceUndesiredStaticImportUsages( | ||
CompilationUnitTree tree, | ||
ImmutableTable<String, String, UndesiredStaticImport> undesiredStaticImports, | ||
VisitorState state) { | ||
new TreeScanner<@Nullable Void, @Nullable Void>() { | ||
@Override | ||
public @Nullable Void visitIdentifier(IdentifierTree node, @Nullable Void unused) { | ||
Symbol symbol = ASTHelpers.getSymbol(node); | ||
if (symbol != null) { | ||
Check warning on line 190 in error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/NonStaticImport.java GitHub Actions / pitestA change can be made to line 190 without causing a test to fail
|
||
UndesiredStaticImport staticImport = | ||
undesiredStaticImports.get( | ||
symbol.owner.getQualifiedName().toString(), symbol.name.toString()); | ||
if (staticImport != null) { | ||
SuggestedFix.Builder fix = staticImport.fixBuilder(); | ||
fix.prefixWith(node, SuggestedFixes.qualifyType(state, fix, symbol.owner) + '.'); | ||
} | ||
} | ||
|
||
return super.visitIdentifier(node, unused); | ||
Check warning on line 200 in error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/NonStaticImport.java GitHub Actions / pitestA change can be made to line 200 without causing a test to fail
|
||
} | ||
}.scan(tree, null); | ||
} | ||
|
||
@AutoValue | ||
abstract static class UndesiredStaticImport { | ||
abstract ImportTree importTree(); | ||
|
||
abstract SuggestedFix.Builder fixBuilder(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,10 @@ | |
|
||
import static com.google.errorprone.BugPattern.LinkType.CUSTOM; | ||
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; | ||
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION; | ||
import static com.google.errorprone.BugPattern.StandardTags.STYLE; | ||
import static java.util.Objects.requireNonNull; | ||
import static tech.picnic.errorprone.bugpatterns.NonStaticImport.NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS; | ||
import static tech.picnic.errorprone.bugpatterns.NonStaticImport.NON_STATIC_IMPORT_CANDIDATE_MEMBERS; | ||
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; | ||
|
||
import com.google.auto.service.AutoService; | ||
|
@@ -27,35 +29,30 @@ | |
import com.sun.tools.javac.code.Type; | ||
import java.util.Optional; | ||
|
||
/** | ||
* A {@link BugChecker} that flags methods and constants that can and should be statically imported. | ||
*/ | ||
/** A {@link BugChecker} that flags type members that can and should be statically imported. */ | ||
// XXX: This check is closely linked to `NonStaticImport`. Consider merging the two. | ||
// XXX: Tricky cases: | ||
// - `org.springframework.http.HttpStatus` (not always an improvement, and `valueOf` must | ||
// certainly be excluded) | ||
// - `com.google.common.collect.Tables` | ||
// - `ch.qos.logback.classic.Level.{DEBUG, ERROR, INFO, TRACE, WARN"}` | ||
// XXX: Also introduce a check that disallows static imports of certain methods. Candidates: | ||
// - `com.google.common.base.Strings` | ||
// - `java.util.Optional.empty` | ||
// - `java.util.Locale.ROOT` | ||
// - `ZoneOffset.ofHours` and other `ofXXX`-style methods. | ||
// - `java.time.Clock`. | ||
// - Several other `java.time` classes. | ||
// - Likely any of `*.{ZERO, ONE, MIX, MAX, MIN_VALUE, MAX_VALUE}`. | ||
// - `ch.qos.logback.classic.Level.{DEBUG, ERROR, INFO, TRACE, WARN}` | ||
@AutoService(BugChecker.class) | ||
@BugPattern( | ||
summary = "Identifier should be statically imported", | ||
link = BUG_PATTERNS_BASE_URL + "StaticImport", | ||
linkType = CUSTOM, | ||
severity = SUGGESTION, | ||
tags = SIMPLIFICATION) | ||
tags = STYLE) | ||
public final class StaticImport extends BugChecker implements MemberSelectTreeMatcher { | ||
private static final long serialVersionUID = 1L; | ||
|
||
/** | ||
* Types whose members should be statically imported, unless exempted by {@link | ||
* #STATIC_IMPORT_EXEMPTED_MEMBERS} or {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS}. | ||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_MEMBERS} or {@link | ||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS}. | ||
* | ||
* <p>Types listed here should be mutually exclusive with {@link | ||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_TYPES}. | ||
*/ | ||
@VisibleForTesting | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one's still |
||
static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_TYPES = | ||
|
@@ -104,8 +101,20 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa | |
"reactor.function.TupleUtils", | ||
"tech.picnic.errorprone.bugpatterns.util.MoreTypes"); | ||
|
||
/** Type members that should be statically imported. */ | ||
@VisibleForTesting | ||
/** | ||
* Type members that should be statically imported. | ||
* | ||
* <p>Please note that: | ||
* | ||
* <ul> | ||
* <li>Types listed by {@link #STATIC_IMPORT_CANDIDATE_TYPES} should be omitted from this | ||
* collection. | ||
* <li>This collection should be mutually exclusive with {@link | ||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_MEMBERS}. | ||
* <li>This collection should not list members contained in {@link | ||
* NonStaticImport#NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS}. | ||
* </ul> | ||
*/ | ||
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_CANDIDATE_MEMBERS = | ||
ImmutableSetMultimap.<String, String>builder() | ||
.putAll( | ||
|
@@ -143,55 +152,6 @@ public final class StaticImport extends BugChecker implements MemberSelectTreeMa | |
.putAll("com.google.common.collect.Comparators", "emptiesFirst", "emptiesLast") | ||
.build(); | ||
|
||
/** | ||
* Type members that should never be statically imported. | ||
* | ||
* <p>Identifiers listed by {@link #STATIC_IMPORT_EXEMPTED_IDENTIFIERS} should be omitted from | ||
* this collection. | ||
*/ | ||
// XXX: Perhaps the set of exempted `java.util.Collections` methods is too strict. For now any | ||
// method name that could be considered "too vague" or could conceivably mean something else in a | ||
// specific context is left out. | ||
@VisibleForTesting | ||
static final ImmutableSetMultimap<String, String> STATIC_IMPORT_EXEMPTED_MEMBERS = | ||
ImmutableSetMultimap.<String, String>builder() | ||
.put("com.google.common.base.Predicates", "contains") | ||
.put("com.mongodb.client.model.Filters", "empty") | ||
.putAll( | ||
"java.util.Collections", | ||
"addAll", | ||
"copy", | ||
"fill", | ||
"list", | ||
"max", | ||
"min", | ||
"nCopies", | ||
"rotate", | ||
"sort", | ||
"swap") | ||
.putAll("java.util.regex.Pattern", "compile", "matches", "quote") | ||
.put("org.springframework.http.MediaType", "ALL") | ||
.build(); | ||
|
||
/** | ||
* Identifiers that should never be statically imported. | ||
* | ||
* <p>This should be a superset of the identifiers flagged by {@link | ||
* com.google.errorprone.bugpatterns.BadImport}. | ||
*/ | ||
@VisibleForTesting | ||
static final ImmutableSet<String> STATIC_IMPORT_EXEMPTED_IDENTIFIERS = | ||
ImmutableSet.of( | ||
"builder", | ||
"create", | ||
"copyOf", | ||
"from", | ||
"getDefaultInstance", | ||
"INSTANCE", | ||
"newBuilder", | ||
"of", | ||
"valueOf"); | ||
|
||
/** Instantiates a new {@link StaticImport} instance. */ | ||
public StaticImport() {} | ||
|
||
|
@@ -229,13 +189,13 @@ private static boolean isCandidateContext(VisitorState state) { | |
|
||
private static boolean isCandidate(MemberSelectTree tree) { | ||
String identifier = tree.getIdentifier().toString(); | ||
if (STATIC_IMPORT_EXEMPTED_IDENTIFIERS.contains(identifier)) { | ||
if (NON_STATIC_IMPORT_CANDIDATE_IDENTIFIERS.contains(identifier)) { | ||
return false; | ||
} | ||
|
||
Type type = ASTHelpers.getType(tree.getExpression()); | ||
return type != null | ||
&& !STATIC_IMPORT_EXEMPTED_MEMBERS.containsEntry(type.toString(), identifier); | ||
&& !NON_STATIC_IMPORT_CANDIDATE_MEMBERS.containsEntry(type.toString(), identifier); | ||
} | ||
|
||
private static Optional<String> getCandidateSimpleName(StaticImportInfo importInfo) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've decided to defer this to a future PR, but good to be aware that in general suppression is something that needs to be handled manually when using a
CompilationUnitTreeMatcher
and/or aTreeScanner
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another follow-up task we many consider is to merge
NonStaticImport
andStaticImport
into justStaticImport
; they're linked anyway.