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 NonStaticImport check #450

Merged
merged 20 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions error-prone-contrib/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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.googlejavaformat</groupId>
<artifactId>google-java-format</artifactId>
Expand Down
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.
Comment on lines +38 to +39
Copy link
Member

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 a TreeScanner.

Copy link
Member

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 and StaticImport into just StaticImport; they're linked anyway.

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned, let's also apply this to StaticImport.

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

View workflow job for this annotation

GitHub Actions / pitest

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

removed conditional - replaced equality check with true (covered by 2 tests RemoveConditionalMutator_EQUAL_IF)
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe memberIdent or memberIdentifier? That makes it a tiny bit clearer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a member of the aforementioned type. That it's an identifier is apparent from the String type. This also matches the shouldNotBeStaticallyImported parameter names. No very strong opinion, but I think that member was a better name than memberIdentifier.

Copy link
Member

Choose a reason for hiding this comment

The 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

View workflow job for this annotation

GitHub Actions / pitest

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

removed conditional - replaced equality check with true (covered by 2 tests RemoveConditionalMutator_EQUAL_IF)
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

View workflow job for this annotation

GitHub Actions / pitest

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

replaced return value with null for visitIdentifier (covered by 2 tests NullReturnValsMutator)
}
}.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
Expand Up @@ -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;
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one's still @VisibleForTesting.

static final ImmutableSet<String> STATIC_IMPORT_CANDIDATE_TYPES =
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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() {}

Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading