Skip to content

Make TypeParameterQualifier Refaster-aware#5884

Open
Stephan202 wants to merge 2 commits into
google:masterfrom
PicnicSupermarket:sschroevers/make-TypeParameterQualifier-Refaster-aware
Open

Make TypeParameterQualifier Refaster-aware#5884
Stephan202 wants to merge 2 commits into
google:masterfrom
PicnicSupermarket:sschroevers/make-TypeParameterQualifier-Refaster-aware

Conversation

@Stephan202

@Stephan202 Stephan202 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

For method references, there are valid use cases within Refaster template methods.

For method references, there valid use cases within Refaster
`@BeforeTemplate` methods.
@Stephan202

Copy link
Copy Markdown
Contributor Author

See PicnicSupermarket/error-prone-support#2274 for context. An alternative to this change is #5883, though these PRs are not mutually exclusive.

class Test {
@BeforeTemplate
<T extends Enum<T>> Function<T, String> rule() {
return T::name;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just to be clear: This one could use Enum::name, right? It's still reasonable to test it; it's just that the case below is the one in which Refaster needs you to use T?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, sorry: Since (related to my other comment!) this is a @BeforeTemplate, then the goal is to match when people write Foo::name, even though they could write Enum::name instead. So, in a Refaster template, unlike in normal code, you very much do want to be able to write T::name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Exactly :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks. I'm now grasping the other test shows that we continue not to try to support matches on Foo.valueOf(Foo.class, value) (or especially Foo.valueOf(Bar.class, value)!), which is definitely weirder than writing Enum.valueOf(Foo.class, value) :) Now I suppose that we could still want to provide a way to match the latter (to steer people away from it!), but I am happy to stop here now that I hopefully actually understand what's going on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since there is (IIRC) another Error Prone rule (or is it CheckStyle?) that nudges users to using DeclaringType.staticMethod instead of SomeSubType.staticMethod, I suspect there is less need for supporting this. But I also can't come up with a strong argument for not supporting this. Sometimes those static methods are "overridden".

Note that if we do also want to allow this, the PR actually becomes simpler, and the code likely more efficient. Cause we can then simply set:

@BugPattern(
    summary = "Type parameter used as type qualifier",
    severity = ERROR,
    suppressionAnnotations = BeforeTemplate.class)

That would make the change set class with #5883, but that's not a biggie.

Let's unresolve this thread for input by @cushon.

private static boolean isInRefasterBeforeTemplate(VisitorState state) {
MethodTree enclosingMethod = findEnclosingMethod(state);
return enclosingMethod != null
&& hasAnnotation(enclosingMethod, BEFORE_TEMPLATE_ANNOTATION, state);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Obvious question that might have an obvious answer: Would it make sense to check @AfterTemplate, too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good question. I thought I convinced myself that the answer is "no", but now that I think back to what I tested: the answer is actually "yes". T::method will be replaced with ConcreteType::method, and one may reasonably want that in case e.g. a rule wishes to rewrite some context around arbitrary T::method. I've pushed another commit. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(Generalized the PR description accordingly.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In hindsight, I guess that my other thread implies that an @AfterTemplate could always use BoundOfTheTypeParameter::method instead of T::method.

The former might actually be safer: If BoundOfTheTypeParameter::method compiles in the template, then it's very likely to compile in the output, whereas T::method could perhaps become ambiguous when replaced with ConcreteType::method. For example, Enum::valueOf is clearly valueOf(Class, String), but ConcreteType::valueOf could be either that or the valueOf(String) method generated on the enum class ConcreteType.

The former also feels less like a spiritual violation of NonCanonicalType / NonCanonicalStaticImport. But when we discussed that very thing (in the context of List::stream vs. Collection::stream) internally a couple months ago in bug 411135416, we decided that we didn't actually care :)

Those don't seem like deal-breakers. If someone wants to produce ConcreteType::name, then sure, I guess?

It does seem believable that there would be cases in which we don't have a specific known type for T, in which case generating a literal "T::whatever" would be bad. Still... I guess it's OK-ish? Or we should catch it inside Refaster if we don't already? But I guess I could see that as a somewhat more compelling reason to have not allowed T qualifiers in the @AfterTemplate.

And finally: I am belatedly recognizing that your implementation and your test take the position that there might very well not be a good case for either a @BeforeTemplate or an @AfterTemplate that contains T.someMethod(): someMethod would have to be a static method that is probably better referred to through the actual declaring type, as in the other comment thread. Additionally, the ban of T.<anything> has the virtue of preserving the ban on T.Foo in the signature (return types / parameters / type parameters) of the Refaster template, allowing qualification on T only for method references. (So my earlier Turbine experiments were focused on a case whose behavior you haven't even been proposing to change!)

After all that, I'm probably more on the side of your original approach of omitting this from the PR :) But I don't feel confident after all the knots I've unnecessarily tied myself into here. Let's at least wait to see if @cushon has higher-level concerns about the direction of the two PRs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm totally fine either way :). You make a compelling argument, though dropping the @AfterTemplate exemption does make it harder to write a rule that matches-without-modification of that particular sub-expression. There does exist an escape hatch for that, though, through the introduction of a @Matches(IsSomeMethodReference.class)-guarded template parameter. Significantly more work for the user, but as this would be rare, it is not a strong argument for keeping the PR as-is.

If we drop the second commit, let's remember to also revert the PR description body (as Copybara transfers that to the final commit message, I've observed).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good call on the PR description.

I'm realizing that the bottom line here might be this: Turbine doesn't care about anything that this CL affects.

@cpovirk

cpovirk commented Jun 17, 2026

Copy link
Copy Markdown
Member

I also observe that:

  • At least in the case I tested, our google3 Refaster integration does run Turbine on the Refaster templates, so we would care about references there.
    • [edit: I was wrong. See below.]
  • We could still rely on Turbine to reject them itself. The nightmare scenario would presumably arise if you were to successfully build your own code (but not any tests for it or anything else that depends on it!) and then submit, only for your users to break when they run Turbine on it for the first time. Error Prone protects against that edge case.
  • There was a time when Turbine crashed on such code, so we're already in a better position now that it's been reporting a proper error for a long time.
  • Perhaps we could allow suppression for expressions, which Turbine doesn't care about, but not for types—at least not in signatures, since Turbine looks at those?

@cpovirk

cpovirk commented Jun 17, 2026

Copy link
Copy Markdown
Member

At least in the case I tested, our google3 Refaster integration does run Turbine on the Refaster templates, so we would care about references there.

My mistake: It does not run Turbine on them (in the case I tested); I just saw an error and assumed it was from Turbine when in fact it was from... TypeParameterQualifier :)

I do notice that (for better or for worse) it's currently possible to disable TypeParameterQualifier with -Xep:TypeParameterQualifier:OFF. It's possible that, if we don't do #5884, we should instead eliminate the way of disabling it. Or maybe the current state of affairs is a pragmatic compromise, one that would get even better if we were to merge this particular PR?

@cpovirk

cpovirk commented Jun 17, 2026

Copy link
Copy Markdown
Member

And wait... :)

If I insert the a Refaster template of...

    @BeforeTemplate
    private <T extends java.util.Map<?, ?>> Object o(T.Entry<?, ?> t) {
      return t;
    }

...into our EnumsShortcuts, then I get Refaster crashes in a couple targets, including OrderingShortcutsTest_refasters. This might be the root failure:

Jun 17, 2026 11:11:34 AM com.google.devtools.javatools.refactory.MultipassRefactoring analyzeCompilationUnit
SEVERE: Unexpected error in javac analysis of , pass: Test
java.lang.LinkageError
  at com.google.errorprone.refaster.Template.callCheckMethod(Template.java:547)
  at com.google.errorprone.refaster.Template.infer(Template.java:478)
  at com.google.errorprone.refaster.Template.typecheck(Template.java:209)
  at com.google.errorprone.refaster.ExpressionTemplate$2.apply(ExpressionTemplate.java:208)
  at com.google.errorprone.refaster.ExpressionTemplate$2.apply(ExpressionTemplate.java:170)
  at com.google.errorprone.refaster.Choice$2.mapIfPresent(Choice.java:126)
  at com.google.errorprone.refaster.ExpressionTemplate.unify(ExpressionTemplate.java:169)
  at com.google.errorprone.refaster.ExpressionTemplate.match(ExpressionTemplate.java:128)
  at com.google.errorprone.refaster.RefasterScanner.scan(RefasterScanner.java:108)
  at com.google.errorprone.refaster.RefasterScanner.scan(RefasterScanner.java:52)
  at jdk.compiler/com.sun.source.util.TreeScanner.scanAndReduce(TreeScanner.java:95)
  at jdk.compiler/com.sun.source.util.TreeScanner.visitPackage(TreeScanner.java:168)
  at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCPackageDecl.accept(JCTree.java:664)
  at jdk.compiler/com.sun.source.util.TreeScanner.scan(TreeScanner.java:91)
  at com.google.errorprone.refaster.RefasterScanner.scan(RefasterScanner.java:133)
  at com.google.errorprone.refaster.RefasterScanner.scan(RefasterScanner.java:52)
  at jdk.compiler/com.sun.source.util.TreeScanner.visitCompilationUnit(TreeScanner.java:149)
  at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCCompilationUnit.accept(JCTree.java:627)
  at jdk.compiler/com.sun.source.util.TreeScanner.scan(TreeScanner.java:91)
  at com.google.errorprone.refaster.RefasterScanner.scan(RefasterScanner.java:133)
  at com.google.errorprone.refaster.RefasterRule.apply(RefasterRule.java:139)
  at com.google.devtools.javatools.refactory.refaster.RefasterMatcher.analyzeFile(RefasterMatcher.java:71)
  at com.google.devtools.javatools.refactory.refaster.RefasterMatcher.analyzeFile(RefasterMatcher.java:53)
  at com.google.devtools.javatools.refactory.refaster.RefasterMatcher.analyzeCompilationUnit(RefasterMatcher.java:41)
  at com.google.devtools.javatools.refactory.Refactorings.refactorAndReturnReplacements(Refactorings.java:108)
  at com.google.devtools.javatools.refactory.MultipassRefactoring.lambda$analyzeCompilationUnit$0(MultipassRefactoring.java:130)
  at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328)
  at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
  at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
  at java.base/java.lang.Thread.run(Thread.java:1547)
Caused by: java.lang.AssertionError: isSubtype PACKAGE
  at jdk.compiler/com.sun.tools.javac.code.Types$4.visitType(Types.java:1130)
  at jdk.compiler/com.sun.tools.javac.code.Types$4.visitType(Types.java:1107)
  at jdk.compiler/com.sun.tools.javac.code.Types$DefaultTypeVisitor.visitPackageType(Types.java:4941)
  at jdk.compiler/com.sun.tools.javac.code.Type$PackageType.accept(Type.java:1592)
  at jdk.compiler/com.sun.tools.javac.code.Types$DefaultTypeVisitor.visit(Types.java:4936)
  at jdk.compiler/com.sun.tools.javac.code.Types.isSubtype(Types.java:1103)
  at jdk.compiler/com.sun.tools.javac.code.Types.isSubtypeUncheckedInternal(Types.java:1029)
  at jdk.compiler/com.sun.tools.javac.code.Types.isSubtypeUnchecked(Types.java:1015)
  at jdk.compiler/com.sun.tools.javac.code.Types.isConvertible(Types.java:610)
  at jdk.compiler/com.sun.tools.javac.comp.Resolve$MethodCheckContext.compatible(Resolve.java:1052)
  at jdk.compiler/com.sun.tools.javac.comp.Check.checkType(Check.java:615)
  at jdk.compiler/com.sun.tools.javac.comp.Attr$ResultInfo.check(Attr.java:531)
  at jdk.compiler/com.sun.tools.javac.comp.Resolve$MethodResultInfo.check(Resolve.java:1098)
  at jdk.compiler/com.sun.tools.javac.comp.Resolve$4.checkArg(Resolve.java:918)
  at jdk.compiler/com.sun.tools.javac.comp.Resolve$AbstractMethodCheck.argumentsAcceptable(Resolve.java:803)
  at jdk.compiler/com.sun.tools.javac.comp.Resolve$4.argumentsAcceptable(Resolve.java:927)
  at jdk.compiler/com.sun.tools.javac.comp.Infer.instantiateMethod(Infer.java:184)
  at jdk.compiler/com.sun.tools.javac.comp.Resolve.rawInstantiate(Resolve.java:633)
  at jdk.compiler/com.sun.tools.javac.comp.Resolve.checkMethod(Resolve.java:672)
  at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
  at java.base/java.lang.reflect.Method.invoke(Method.java:565)
  at com.google.errorprone.refaster.Template.callCheckMethod(Template.java:532)
  ... 29 more

@cpovirk

cpovirk commented Jun 17, 2026

Copy link
Copy Markdown
Member

For the crash, Gemini says:

--- a/core/src/main/java/com/google/errorprone/refaster/Template.java  2026-01-29 08:07:06.000000000 -0500
+++ b/core/src/main/java/com/google/errorprone/refaster/Template.java  2026-06-17 14:46:11.000000000 -0400
@@ -203,6 +203,12 @@
       Warner warner,
       List<Type> expectedTypes,
       List<Type> actualTypes) {
+    for (Type type : actualTypes) {
+      TypeTag tag = type.getTag();
+      if (tag == TypeTag.PACKAGE || tag == TypeTag.METHOD || tag == TypeTag.MODULE || tag == TypeTag.FORALL) {
+        return Optional.empty();
+      }
+    }
     try {
       ImmutableList<UTypeVar> freeTypeVars = freeTypeVars(unifier);
       var unused =

Maybe? I didn't test whether the template from PicnicSupermarket/error-prone-support#2274 would still work with that change.

@cpovirk

cpovirk commented Jun 17, 2026

Copy link
Copy Markdown
Member

That change breaks a number of existing Refaster tests.

[edit: Wrong again; see below.]

@cpovirk

cpovirk commented Jun 17, 2026

Copy link
Copy Markdown
Member

Wait, no, sorry again: What breaks the tests is my test template with Map.Entry, which matches null (which is a Map.Entry!) all over. The actual attempted fix doesn't break any tests that I found.

@Stephan202 Stephan202 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tnx for the review @cpovirk!

Maybe? I didn't test whether the template from PicnicSupermarket/error-prone-support#2274 would still work with that change.

I can quickly try that in a bit; we have test cases for each rule, so shouldn't be hard.

class Test {
@BeforeTemplate
<T extends Enum<T>> Function<T, String> rule() {
return T::name;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Exactly :)

private static boolean isInRefasterBeforeTemplate(VisitorState state) {
MethodTree enclosingMethod = findEnclosingMethod(state);
return enclosingMethod != null
&& hasAnnotation(enclosingMethod, BEFORE_TEMPLATE_ANNOTATION, state);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good question. I thought I convinced myself that the answer is "no", but now that I think back to what I tested: the answer is actually "yes". T::method will be replaced with ConcreteType::method, and one may reasonably want that in case e.g. a rule wishes to rewrite some context around arbitrary T::method. I've pushed another commit. :)

@Stephan202

Copy link
Copy Markdown
Contributor Author

Maybe? I didn't test whether the template from PicnicSupermarket/error-prone-support#2274 would still work with that change.

I can quickly try that in a bit; we have test cases for each rule, so shouldn't be hard.

That change doesn't break our build ✔️

@cpovirk cpovirk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for testing the possible fix!

private static boolean isInRefasterBeforeTemplate(VisitorState state) {
MethodTree enclosingMethod = findEnclosingMethod(state);
return enclosingMethod != null
&& hasAnnotation(enclosingMethod, BEFORE_TEMPLATE_ANNOTATION, state);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In hindsight, I guess that my other thread implies that an @AfterTemplate could always use BoundOfTheTypeParameter::method instead of T::method.

The former might actually be safer: If BoundOfTheTypeParameter::method compiles in the template, then it's very likely to compile in the output, whereas T::method could perhaps become ambiguous when replaced with ConcreteType::method. For example, Enum::valueOf is clearly valueOf(Class, String), but ConcreteType::valueOf could be either that or the valueOf(String) method generated on the enum class ConcreteType.

The former also feels less like a spiritual violation of NonCanonicalType / NonCanonicalStaticImport. But when we discussed that very thing (in the context of List::stream vs. Collection::stream) internally a couple months ago in bug 411135416, we decided that we didn't actually care :)

Those don't seem like deal-breakers. If someone wants to produce ConcreteType::name, then sure, I guess?

It does seem believable that there would be cases in which we don't have a specific known type for T, in which case generating a literal "T::whatever" would be bad. Still... I guess it's OK-ish? Or we should catch it inside Refaster if we don't already? But I guess I could see that as a somewhat more compelling reason to have not allowed T qualifiers in the @AfterTemplate.

And finally: I am belatedly recognizing that your implementation and your test take the position that there might very well not be a good case for either a @BeforeTemplate or an @AfterTemplate that contains T.someMethod(): someMethod would have to be a static method that is probably better referred to through the actual declaring type, as in the other comment thread. Additionally, the ban of T.<anything> has the virtue of preserving the ban on T.Foo in the signature (return types / parameters / type parameters) of the Refaster template, allowing qualification on T only for method references. (So my earlier Turbine experiments were focused on a case whose behavior you haven't even been proposing to change!)

After all that, I'm probably more on the side of your original approach of omitting this from the PR :) But I don't feel confident after all the knots I've unnecessarily tied myself into here. Let's at least wait to see if @cushon has higher-level concerns about the direction of the two PRs.

class Test {
@BeforeTemplate
<T extends Enum<T>> Function<T, String> rule() {
return T::name;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks. I'm now grasping the other test shows that we continue not to try to support matches on Foo.valueOf(Foo.class, value) (or especially Foo.valueOf(Bar.class, value)!), which is definitely weirder than writing Enum.valueOf(Foo.class, value) :) Now I suppose that we could still want to provide a way to match the latter (to steer people away from it!), but I am happy to stop here now that I hopefully actually understand what's going on.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants