jhipster / prettier-java

Prettier Java Plugin
http://www.jhipster.tech/prettier-java/
Apache License 2.0
1.08k stars 103 forks source link

feat: JDK 22 string templates #640

Closed jtkiesel closed 7 months ago

jtkiesel commented 8 months ago

What changed with this PR:

JDK 22 string templates are now supported. The printing of certain more complex embedded expressions is far from perfect, but I didn't want that to hold up this support and can make improvements to that in a subsequent PR.

Example

Input

class TemplateExpression {

  String info = STR."My name is \{name}";

  String s = STR."\{x} + \{y} = \{x + y}";

  String s = STR."You have a \{getOfferType()} waiting for you!";

  String msg = STR."The file \{filePath} \{file.exists() ? "does" : "does not"} exist";

  String time = STR."The time is \{
    // The java.time.format package is very useful
    DateTimeFormatter
      .ofPattern("HH:mm:ss")
      .format(LocalTime.now())
  } right now";

  String data = STR."\{index++}, \{index++}, \{index++}, \{index++}";

  String s = STR."\{fruit[0]}, \{STR."\{fruit[1]}, \{fruit[2]}"}";

  String html = STR."""
    <html>
      <head>
        <title>\{title}</title>
      </head>
      <body>
        <p>\{text}</p>
      </body>
    </html>
    """;

  String table = STR."""
    Description  Width  Height  Area
    \{zone[0].name}  \{zone[0].width}  \{zone[0].height}     \{zone[0].area()}
    \{zone[1].name}  \{zone[1].width}  \{zone[1].height}     \{zone[1].area()}
    \{zone[2].name}  \{zone[2].width}  \{zone[2].height}     \{zone[2].area()}
    Total \{zone[0].area() + zone[1].area() + zone[2].area()}
    """;

  String table = FMT."""
    Description     Width    Height     Area
    %-12s\{zone[0].name}  %7.2f\{zone[0].width}  %7.2f\{zone[0].height}     %7.2f\{zone[0].area()}
    %-12s\{zone[1].name}  %7.2f\{zone[1].width}  %7.2f\{zone[1].height}     %7.2f\{zone[1].area()}
    %-12s\{zone[2].name}  %7.2f\{zone[2].width}  %7.2f\{zone[2].height}     %7.2f\{zone[2].area()}
    \{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()}
    """;

  PreparedStatement ps = DB."SELECT * FROM Person p WHERE p.last_name = \{name}";
}

Output

class TemplateExpression {

  String info = STR."My name is \{name}";

  String s = STR."\{x} + \{y} = \{x + y}";

  String s = STR."You have a \{getOfferType()} waiting for you!";

  String msg =
    STR."The file \{filePath} \{file.exists() ? "does" : "does not"} exist";

  String time =
    STR."The time is \{
      // The java.time.format package is very useful
      DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now())
    } right now";

  String data = STR."\{index++}, \{index++}, \{index++}, \{index++}";

  String s = STR."\{fruit[0]}, \{STR."\{fruit[1]}, \{fruit[2]}"}";

  String html =
    STR."""
    <html>
      <head>
        <title>\{title}</title>
      </head>
      <body>
        <p>\{text}</p>
      </body>
    </html>
    """;

  String table =
    STR."""
    Description  Width  Height  Area
    \{zone[0].name}  \{zone[0].width}  \{zone[0].height}     \{zone[0].area()}
    \{zone[1].name}  \{zone[1].width}  \{zone[1].height}     \{zone[1].area()}
    \{zone[2].name}  \{zone[2].width}  \{zone[2].height}     \{zone[2].area()}
    Total \{zone[0].area() + zone[1].area() + zone[2].area()}
    """;

  String table =
    FMT."""
    Description     Width    Height     Area
    %-12s\{zone[0].name}  %7.2f\{zone[0].width}  %7.2f\{
      zone[0].height
    }     %7.2f\{zone[0].area()}
    %-12s\{zone[1].name}  %7.2f\{zone[1].width}  %7.2f\{
      zone[1].height
    }     %7.2f\{zone[1].area()}
    %-12s\{zone[2].name}  %7.2f\{zone[2].width}  %7.2f\{
      zone[2].height
    }     %7.2f\{zone[2].area()}
    \{" ".repeat(28)} Total %7.2f\{
      zone[0].area() + zone[1].area() + zone[2].area()
    }
    """;

  PreparedStatement ps =
    DB."SELECT * FROM Person p WHERE p.last_name = \{name}";
}

Relative issues or prs:

Closes #618

clementdessoude commented 8 months ago

I was struggling with if/else blocks when I checked a few weeks/months ago. I don't remember exactly, but I think that this snippet was causing some trouble :/

it("should still parse simple if/else", () => {
    const input = `
      String formatted = "unknown";
      if (o instanceof Integer i) {
          formatted = String.format("int %d", i);
      } else if (o instanceof Double d) {
          formatted = String.format("double %f", d);
      }
      `;

    javaParser.parse(input, "methodDeclaration");
    expect(() => javaParser.parse(input, "methodDeclaration")).to.not.throw();
  });
jtkiesel commented 8 months ago

@clementdessoude I ran into something similar to that as well. I ended up resolving it by leveraging Chevrotain's lexer modes (see the changes I made to allTokens in tokens.js). Still running into an issue with idempotency when the embedded expression contains comments, which I'll need to look into.

jtkiesel commented 7 months ago

The issue I mentioned above is now fixed. Tokens containing newlines are meant to be split into multiple docs with the newlines replaced with the literalline doc. When I wasn't doing so, I noticed that Prettier was determining line length (and therefore determining where to break) as if the newlines were nonexistent. Luckily, Prettier had a utility function that they use for this purpose that I was easily able to use.

clementdessoude commented 7 months ago

I will look at this tomorrow :)