square / javapoet

A Java API for generating .java source files.
Apache License 2.0
10.83k stars 1.38k forks source link

Type parameters shadow nested return types with the same name #997

Open skinny85 opened 10 months ago

skinny85 commented 10 months ago

First of all, thank you for creating and maintaining JavaPoet, it's a great library 🙂.

I'm the author of Jilt, a Java annotation processor that uses JavaPoet for generating code. It's during work on this library that I've ran into this interesting edge case.

Let me give you a short introduction to Jilt. The purpose of this library is to automatically generate classes that implement the Builder design pattern. One of the important capabilities of the library is generating so-called Staged Builders. The idea there is to generate a separate interface for each required property of the class being built, and forming a chain out of them, and this way enforcing, at compile-time, that each required property is provided before the final instance is built.

A specific example might be helpful. Let's say we have the following simple value class:

public final class FullName {
    public final String firstName, lastName;

    public FullName(String firstName, String lastName) {
        this.firstName = Objects.requireNonNull(firstName);
        this.lastName = Objects.requireNonNull(lastName);
    }
}

Jilt would generate the following code (all using JavaPoet, of course) for a Staged Builder for that class:

public interface FullNameBuilders {
  interface FirstName {
    LastName firstName(String firstName);
  }

  interface LastName {
    Optionals lastName(String lastName);
  }

  interface Optionals {
    FullName build();
  }
}

public class FullNameBuilder implements FullNameBuilders.FirstName,
    FullNameBuilders.LastName, FullNameBuilders.Optionals {
  private String firstName;

  private String lastName;

  private FullNameBuilder() {
  }

  public static FullNameBuilders.FirstName fullName() {
    return new FullNameBuilder();
  }

  public FullNameBuilders.LastName firstName(String firstName) {
    this.firstName = firstName;
    return this;
  }

  public FullNameBuilders.Optionals lastName(String lastName) {
    this.lastName = lastName;
    return this;
  }

  public FullName build() {
    return new FullName(firstName, lastName);
  }
}

If you notice, in the interfaces nested in FullNameBuilders, the return types that JavaPoet generates for the method declarations are not qualified with the name of the outer interface - so, it's LastName firstName(String firstName);, and not FullNameBuilders.LastName firstName(String firstName);. Also, the names of the interfaces nested in FullNameBuilders are (by default) simply the capitalized names of the fields. Both of these will be important in a second 🙂.

So far so good. But things become more interesting when the class being built is generic (has type parameters). When that happens, the per-property interfaces also need to be parametrized, with the same parameters as the built class.

And if the name of the type parameter and the (capitalized) name of the field are the same, the type parameter shadows the next interface in the chain, and the code doesn't compile!

Let me show another example, illustrating the problem:

public final class Pair<First, Second> {
    public final First first;
    public final Second second;

    public Pair(First first, Second second) {
        this.first = first;
        this.second = second;
    }
}

The interfaces generated for the properties are (I'll skip the actual Builder class, as it doesn't really matter here):

public interface PairBuilders {
  interface First<First, Second> {
    Second<First, Second> first(First first);
  }

  interface Second<First, Second> {
    Build<First, Second> second(Second second);
  }

  interface Build<First, Second> {
    Pair<First, Second> build();
  }
}

In the method declaration Second<First, Second> first(First first); in PairBuilders.First, the Second return type unfortunately does not refer to PairBuilders.Second, but to the Second type parameter, which shadows the unqualified name Second in this case. This, of course, fails to compile.

The solution here would be to update the algorithm used by JavaPoet to resolve type names, to make sure it takes potentially shadowing type parameters into account, and generate the first() method with the return type PairBuilders.Second instead, like this:

public interface PairBuilders {
  interface First<First, Second> {
    PairBuilders.Second<First, Second> first(First first);
  }

  interface Second<First, Second> {
    Build<First, Second> second(Second second);
  }

  interface Build<First, Second> {
    Pair<First, Second> build();
  }
}

Let me know if this makes sense, and thanks again for the great library!