Skip to content

Conversation

@kelvinou01
Copy link
Contributor

@kelvinou01 kelvinou01 commented Dec 12, 2025

Before this PR

A few bugs in suppressible-error-prone:

  • AllBugCheckers wasn't loading any plugin checks
  • RemoveUnused mode didn't understand suppressions using alt-names, e.g. SuppressWarnings("unused"), leading it to overzealously remove suppressions

After this PR

  • AllBugCheckers loads plugin checks
  • RemoveUnused mode understands alt-names

==COMMIT_MSG==
Fix removeUnused mode not loading plugin checks and alt names
==COMMIT_MSG==

Possible downsides?

Probably next PR: RemoveUnused should not remove human-made suppressions. For example, a human-made suppression on SafeLoggingPropagation for a method which is safe to log. RemoveUnused + Apply should not remove the suppression and add the @Unsafe annotation.

If needed, human-authored suppressions can be removed on an allow-list basis. But the correct and performant removal of robot-added suppressions should be the focus here.

@changelog-app
Copy link

changelog-app bot commented Dec 12, 2025

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

Fix removeUnused mode over- and under-removing suppressions

Check the box to generate changelog(s)

  • Generate changelog entry

@kelvinou01 kelvinou01 force-pushed the okelvin/fix-remove-unused branch from 082b6c1 to e507d60 Compare December 15, 2025 11:32
@kelvinou01 kelvinou01 changed the title Fix removeUnused mode over- and under-removing suppressions Fix removeUnused mode not loading plugin checks, and not respecting alt names Dec 15, 2025
@kelvinou01 kelvinou01 changed the title Fix removeUnused mode not loading plugin checks, and not respecting alt names Fix removeUnused mode not loading plugin checks and alt names Dec 15, 2025
@changelog-app
Copy link

changelog-app bot commented Dec 15, 2025

Successfully generated changelog entry!

Need to regenerate?

Simply interact with the changelog bot comment again to regenerate these entries.


📋Changelog Preview

🐛 Fixes

  • Fix removeUnused mode over- and under-removing suppressions (#278)

'''.stripIndent(true)

when:
runTasksSuccessfully('compileAllErrorProne', '-PerrorProneRemoveUnused=ArrayToString')
Copy link
Contributor Author

@kelvinou01 kelvinou01 Dec 15, 2025

Choose a reason for hiding this comment

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

RemoveUnused doesn't take arguments — this argument was being ignored

@kelvinou01 kelvinou01 requested a review from CRogers December 15, 2025 18:44
Copy link
Contributor

@aldexis aldexis left a comment

Choose a reason for hiding this comment

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

I tried to have a look, but I'm not familiar enough with the remove unused logic to confidently review the actual logic changes. Best to have @CRogers have an eye on this as well

@BugPattern(
severity = BugPattern.SeverityLevel.ERROR,
summary = "This check is meant only for testing suppressible-error-prone functionality")
public final class ShouldNotReturn extends BugChecker implements MethodTreeMatcher {
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you use this anywhere?

ServiceLoader.load(BugChecker.class).stream().map(ServiceLoader.Provider::get);
Stream<String> pluginCheckNames =
StreamEx.of(pluginChecks).flatMap(bugchecker -> bugchecker.allNames().stream());
private static Supplier<Map<String, Set<String>>> canonicalToAllNames = VisitorState.memoize(state -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private static Supplier<Map<String, Set<String>>> canonicalToAllNames = VisitorState.memoize(state -> {
private static final Supplier<Map<String, Set<String>>> canonicalToAllNames = VisitorState.memoize(state -> {


public static Set<String> allBugcheckerNames() {
return cachedAllBugcheckerNames.get();
private static Supplier<Set<String>> allNames =
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private static Supplier<Set<String>> allNames =
private static final Supplier<Set<String>> allNames =

.flatMap(entry -> entry.getValue().stream())
.collect(Collectors.toSet()));

private static Supplier<Map<String, Set<String>>> nameToPossibleCanonicalNames =
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private static Supplier<Map<String, Set<String>>> nameToPossibleCanonicalNames =
private static final Supplier<Map<String, Set<String>>> nameToPossibleCanonicalNames =

.flatMap(entry -> entry.getValue().stream())
.collect(Collectors.toSet()));

private static Supplier<Map<String, Set<String>>> nameToPossibleCanonicalNames =
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to have multiple checks share the same alt name? I imagine so, though it's also pretty surprising

Comment on lines +44 to +45
declaration, state, suppression -> !AllErrorprones.allBugcheckerNames(state)
.contains(suppression));
Copy link
Contributor

Choose a reason for hiding this comment

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

You were previously removing the for-rollout prefix and aren't doing so anymore. Is that intentional?

Comment on lines +98 to +104
String existingSuppression = AnnotationUtils.annotationStringValues(
SuppressWarningsUtils.getSuppressWarnings(declaration)
.get())
.filter(suppression -> allNames.contains(SuppressWarningsUtils.stripForRollout(suppression)))
.collect(MoreCollectors.first())
.get();
FIXES.getExisting(declaration).addSuppression(existingSuppression);
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't you keep all the ones that might already exist, rather than just the first one? (especially the non-rollout ones)

Comment on lines +98 to +103
String existingSuppression = AnnotationUtils.annotationStringValues(
SuppressWarningsUtils.getSuppressWarnings(declaration)
.get())
.filter(suppression -> allNames.contains(SuppressWarningsUtils.stripForRollout(suppression)))
.collect(MoreCollectors.first())
.get();
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this leverage suppressionsOn in some way?

.map(Provider::get)
.collect(Collectors.toList());
EntryStream<String, Set<String>> pluginCheckNames =
StreamEx.of(pluginChecks).mapToEntry(BugChecker::canonicalName, BugChecker::allNames);
Copy link
Contributor

Choose a reason for hiding this comment

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

BugChecker#allNames will return for-rollout:Name if we've patched the errorprone classes in the artifact transform.

Copy link
Contributor

Choose a reason for hiding this comment

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

Which may be what we want...

// Use the same classloader that Error Prone was loaded from to avoid classloader skew
// when using Error Prone plugins together with the Error Prone javac plugin.
JavacProcessingEnvironment processingEnvironment = JavacProcessingEnvironment.instance(state.context);
ClassLoader loader = processingEnvironment.getProcessorClassLoader();
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if there's any real difference between this and state.getClass().getClassLoader()? The latter does not require interacting with so many compiler APIs or module imports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants