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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
skinny85 opened this issue Jan 27, 2024 · 0 comments
Open

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

skinny85 opened this issue Jan 27, 2024 · 0 comments

Comments

@skinny85
Copy link

skinny85 commented Jan 27, 2024

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!

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

No branches or pull requests

1 participant