diff --git a/packages/java-parser/api.d.ts b/packages/java-parser/api.d.ts index 1db05aea..37c94d48 100644 --- a/packages/java-parser/api.d.ts +++ b/packages/java-parser/api.d.ts @@ -351,6 +351,11 @@ export abstract class JavaCstVisitor implements ICstVisitor { classLiteralSuffix(ctx: ClassLiteralSuffixCtx, param?: IN): OUT; arrayAccessSuffix(ctx: ArrayAccessSuffixCtx, param?: IN): OUT; methodReferenceSuffix(ctx: MethodReferenceSuffixCtx, param?: IN): OUT; + templateArgument(ctx: TemplateArgumentCtx, param?: IN): OUT; + template(ctx: TemplateCtx, param?: IN): OUT; + stringTemplate(ctx: StringTemplateCtx, param?: IN): OUT; + textBlockTemplate(ctx: TextBlockTemplateCtx, param?: IN): OUT; + embeddedExpression(ctx: EmbeddedExpressionCtx, param?: IN): OUT; pattern(ctx: PatternCtx, param?: IN): OUT; typePattern(ctx: TypePatternCtx, param?: IN): OUT; recordPattern(ctx: RecordPatternCtx, param?: IN): OUT; @@ -672,6 +677,11 @@ export abstract class JavaCstVisitorWithDefaults classLiteralSuffix(ctx: ClassLiteralSuffixCtx, param?: IN): OUT; arrayAccessSuffix(ctx: ArrayAccessSuffixCtx, param?: IN): OUT; methodReferenceSuffix(ctx: MethodReferenceSuffixCtx, param?: IN): OUT; + templateArgument(ctx: TemplateArgumentCtx, param?: IN): OUT; + template(ctx: TemplateCtx, param?: IN): OUT; + stringTemplate(ctx: StringTemplateCtx, param?: IN): OUT; + textBlockTemplate(ctx: TextBlockTemplateCtx, param?: IN): OUT; + embeddedExpression(ctx: EmbeddedExpressionCtx, param?: IN): OUT; pattern(ctx: PatternCtx, param?: IN): OUT; typePattern(ctx: TypePatternCtx, param?: IN): OUT; recordPattern(ctx: RecordPatternCtx, param?: IN): OUT; @@ -2999,6 +3009,7 @@ export type PrimarySuffixCtx = { unqualifiedClassInstanceCreationExpression?: UnqualifiedClassInstanceCreationExpressionCstNode[]; typeArguments?: TypeArgumentsCstNode[]; Identifier?: IToken[]; + templateArgument?: TemplateArgumentCstNode[]; methodInvocationSuffix?: MethodInvocationSuffixCstNode[]; classLiteralSuffix?: ClassLiteralSuffixCstNode[]; arrayAccessSuffix?: ArrayAccessSuffixCstNode[]; @@ -3264,6 +3275,60 @@ export type MethodReferenceSuffixCtx = { New?: IToken[]; }; +export interface TemplateArgumentCstNode extends CstNode { + name: "templateArgument"; + children: TemplateArgumentCtx; +} + +export type TemplateArgumentCtx = { + template?: TemplateCstNode[]; + StringLiteral?: IToken[]; + TextBlock?: IToken[]; +}; + +export interface TemplateCstNode extends CstNode { + name: "template"; + children: TemplateCtx; +} + +export type TemplateCtx = { + stringTemplate?: StringTemplateCstNode[]; + textBlockTemplate?: TextBlockTemplateCstNode[]; +}; + +export interface StringTemplateCstNode extends CstNode { + name: "stringTemplate"; + children: StringTemplateCtx; +} + +export type StringTemplateCtx = { + StringTemplateBegin: IToken[]; + embeddedExpression: EmbeddedExpressionCstNode[]; + StringTemplateMid?: IToken[]; + StringTemplateEnd: IToken[]; +}; + +export interface TextBlockTemplateCstNode extends CstNode { + name: "textBlockTemplate"; + children: TextBlockTemplateCtx; +} + +export type TextBlockTemplateCtx = { + TextBlockTemplateBegin: IToken[]; + embeddedExpression: EmbeddedExpressionCstNode[]; + TextBlockTemplateMid?: IToken[]; + TextBlockTemplateEnd: IToken[]; +}; + +export interface EmbeddedExpressionCstNode extends CstNode { + name: "embeddedExpression"; + children: EmbeddedExpressionCtx; +} + +export type EmbeddedExpressionCtx = { + expression?: ExpressionCstNode[]; +}; + export interface PatternCstNode extends CstNode { name: "pattern"; children: PatternCtx; diff --git a/packages/java-parser/src/productions/expressions.js b/packages/java-parser/src/productions/expressions.js index 71b72c7b..47322e65 100644 --- a/packages/java-parser/src/productions/expressions.js +++ b/packages/java-parser/src/productions/expressions.js @@ -231,7 +231,8 @@ export function defineRules($, t) { }); $.CONSUME(t.Identifier); } - } + }, + { ALT: () => $.SUBRULE($.templateArgument) } ]); } }, @@ -247,13 +248,20 @@ export function defineRules($, t) { $.RULE("fqnOrRefType", () => { $.SUBRULE($.fqnOrRefTypePartFirst); - $.MANY2({ - // ".class" is a classLiteralSuffix - GATE: () => - // avoids ambiguity with ".this" and ".new" which are parsed as a primary suffix. - tokenMatcher(this.LA(2).tokenType, t.Class) === false && - tokenMatcher(this.LA(2).tokenType, t.This) === false && - tokenMatcher(this.LA(2).tokenType, t.New) === false, + $.MANY({ + // avoids ambiguity with primary suffixes + GATE: () => { + const nextNextToken = $.LA(2); + return !( + tokenMatcher(nextNextToken, t.Class) || + tokenMatcher(nextNextToken, t.This) || + tokenMatcher(nextNextToken, t.New) || + tokenMatcher(nextNextToken, t.StringLiteral) || + tokenMatcher(nextNextToken, t.TextBlock) || + tokenMatcher(nextNextToken, t.StringTemplateBegin) || + tokenMatcher(nextNextToken, t.TextBlockTemplateBegin) + ); + }, DEF: () => { $.CONSUME(t.Dot); $.SUBRULE2($.fqnOrRefTypePartRest); @@ -505,6 +513,47 @@ export function defineRules($, t) { ]); }); + $.RULE("templateArgument", () => { + $.OR([ + { ALT: () => $.SUBRULE($.template) }, + { ALT: () => $.CONSUME(t.StringLiteral) }, + { ALT: () => $.CONSUME(t.TextBlock) } + ]); + }); + + $.RULE("template", () => { + $.OR([ + { ALT: () => $.SUBRULE($.stringTemplate) }, + { ALT: () => $.SUBRULE($.textBlockTemplate) } + ]); + }); + + $.RULE("stringTemplate", () => { + $.CONSUME(t.StringTemplateBegin); + $.SUBRULE($.embeddedExpression); + $.MANY(() => { + $.CONSUME(t.StringTemplateMid); + $.SUBRULE1($.embeddedExpression); + }); + $.CONSUME(t.StringTemplateEnd); + }); + + $.RULE("textBlockTemplate", () => { + $.CONSUME(t.TextBlockTemplateBegin); + $.SUBRULE($.embeddedExpression); + $.MANY(() => { + $.CONSUME(t.TextBlockTemplateMid); + $.SUBRULE1($.embeddedExpression); + }); + $.CONSUME(t.TextBlockTemplateEnd); + }); + + $.RULE("embeddedExpression", () => { + $.OPTION(() => { + $.SUBRULE($.expression); + }); + }); + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-Pattern $.RULE("pattern", () => { $.OR([ diff --git a/packages/java-parser/src/tokens.js b/packages/java-parser/src/tokens.js index 61d3c937..38cc512d 100644 --- a/packages/java-parser/src/tokens.js +++ b/packages/java-parser/src/tokens.js @@ -45,7 +45,11 @@ FRAGMENT("EscapeSequence", "\\\\[bstnfr\"'\\\\]|{{OctalEscape}}"); // Not using InputCharacter terminology there because CR and LF are already captured in EscapeSequence FRAGMENT( "StringCharacter", - "(?:(?:{{EscapeSequence}})|{{UnicodeInputCharacter}})" + '(?:(?:{{EscapeSequence}})|{{UnicodeEscape}}|(?!["\\\\]).)' +); +FRAGMENT( + "TextBlockCharacter", + "(?:(?:{{EscapeSequence}})|{{UnicodeEscape}}|(?!\\\\).|\\\\?{{LineTerminator}})" ); function matchJavaIdentifier(text, startOffset) { @@ -93,10 +97,18 @@ const Identifier = createTokenOrg({ ) }); -const allTokens = []; +const allTokens = { + modes: { + global: [], + stringTemplate: [], + textBlockTemplate: [] + }, + defaultMode: "global" +}; +const allModes = Object.keys(allTokens.modes); const tokenDictionary = {}; -function createToken(options) { +function createToken(options, modes = allModes) { // TODO create a test to check all the tokenbs have a label defined if (!options.label) { // simple token (e.g operator) @@ -110,7 +122,7 @@ function createToken(options) { } const newTokenType = createTokenOrg(options); - allTokens.push(newTokenType); + modes.forEach(mode => allTokens.modes[mode].push(newTokenType)); tokenDictionary[options.name] = newTokenType; return newTokenType; } @@ -221,14 +233,62 @@ createToken({ createToken({ name: "TextBlock", - pattern: /"""\s*\n(\\"|\s|.)*?"""/ + pattern: MAKE_PATTERN( + '"""[\\x09\\x20\\x0C]*{{LineTerminator}}{{TextBlockCharacter}}*?"""' + ) }); +createToken({ + name: "TextBlockTemplateBegin", + pattern: MAKE_PATTERN('"""{{LineTerminator}}{{TextBlockCharacter}}*?\\\\\\{'), + push_mode: "textBlockTemplate" +}); + +createToken( + { + name: "TextBlockTemplateEnd", + pattern: MAKE_PATTERN('\\}{{TextBlockCharacter}}*?"""'), + pop_mode: true + }, + ["textBlockTemplate"] +); + createToken({ name: "StringLiteral", - pattern: MAKE_PATTERN('"(?:[^\\\\"]|{{StringCharacter}})*"') + pattern: MAKE_PATTERN('"{{StringCharacter}}*?"') }); +createToken({ + name: "StringTemplateBegin", + pattern: MAKE_PATTERN('"{{StringCharacter}}*?\\\\\\{'), + push_mode: "stringTemplate" +}); + +createToken( + { + name: "StringTemplateEnd", + pattern: MAKE_PATTERN('\\}{{StringCharacter}}*?"'), + pop_mode: true + }, + ["stringTemplate"] +); + +createToken( + { + name: "StringTemplateMid", + pattern: MAKE_PATTERN("\\}{{StringCharacter}}*?\\\\\\{") + }, + ["stringTemplate"] +); + +createToken( + { + name: "TextBlockTemplateMid", + pattern: MAKE_PATTERN("\\}{{TextBlockCharacter}}*?\\\\\\{") + }, + ["textBlockTemplate"] +); + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-3.html#jls-3.9 // TODO: how to handle the special rule (see spec above) for "requires" and "transitive" const restrictedKeywords = [ @@ -376,8 +436,16 @@ createToken({ name: "Colon", pattern: ":" }); createToken({ name: "QuestionMark", pattern: "?" }); createToken({ name: "LBrace", pattern: "(", categories: [Separators] }); createToken({ name: "RBrace", pattern: ")", categories: [Separators] }); -createToken({ name: "LCurly", pattern: "{", categories: [Separators] }); -createToken({ name: "RCurly", pattern: "}", categories: [Separators] }); +createToken({ + name: "LCurly", + pattern: "{", + categories: [Separators], + push_mode: allTokens.defaultMode +}); +createToken( + { name: "RCurly", pattern: "}", categories: [Separators], pop_mode: true }, + [allTokens.defaultMode] +); createToken({ name: "LSquare", pattern: "[", categories: [Separators] }); createToken({ name: "RSquare", pattern: "]", categories: [Separators] }); @@ -514,7 +582,7 @@ createToken({ // Identifier must appear AFTER all the keywords to avoid ambiguities. // See: https://github.com/SAP/chevrotain/blob/master/examples/lexer/keywords_vs_identifiers/keywords_vs_identifiers.js -allTokens.push(Identifier); +allModes.forEach(mode => allTokens.modes[mode].push(Identifier)); tokenDictionary["Identifier"] = Identifier; function sortDescLength(arr) { diff --git a/packages/java-parser/test/template-expression-spec.js b/packages/java-parser/test/template-expression-spec.js new file mode 100644 index 00000000..d1033200 --- /dev/null +++ b/packages/java-parser/test/template-expression-spec.js @@ -0,0 +1,112 @@ +import { config, expect } from "chai"; +import javaParser from "../src/index.js"; +import JavaLexer from "../src/lexer.js"; + +config.truncateThreshold = 500; + +describe("Template expressions", () => { + it("should parse string template expression without embedded expressions", () => { + const input = `STR."{firstName}"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse string template expression", () => { + const input = `STR."My name is \\{name}"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse string template expression with empty embedded expression", () => { + const input = `STR."My name is \\{}"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should tokenize string template fragments", () => { + const input = `"begin \\{alpha} mid \\{beta} end"`; + + const { tokens } = JavaLexer.tokenize(input); + + expect(tokens.map(token => token.image)).to.have.members([ + '"begin \\{', + "alpha", + "} mid \\{", + "beta", + '} end"' + ]); + }); + + it("should parse string template expression with binary expression", () => { + const input = `STR."\\{x} + \\{y} = \\{x + y}"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse string template expression with method invocation", () => { + const input = `STR."You have a \\{getOfferType()} waiting for you!"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse string template expression with ternary expression", () => { + const input = `STR."The file \\{filePath} \\{file.exists() ? "does" : "does not"} exist"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse nested string template expression", () => { + const input = `STR."\\{fruit[0]}, \\{STR."\\{fruit[1]}, \\{fruit[2]}"}"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse string template expression with template processor from method invocation", () => { + const input = `my.templateProcessor()."\\{fruit[0]}, \\{STR."\\{fruit[1]}, \\{fruit[2]}"}"`; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse text block template expression without embedded expressions", () => { + const input = ` + STR.""" + text + """ + `; + + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should parse text block template expression", () => { + const input = ` + STR.""" + begin + \\{alpha} + mid + \\{beta} + end + """ + `; + expect(() => javaParser.parse(input, "primary")).to.not.throw(); + }); + + it("should tokenize text block template fragments", () => { + const input = `""" + begin + \\{alpha} + mid + \\{beta} + end + """`; + + const { tokens } = JavaLexer.tokenize(input); + + expect(tokens.map(token => token.image)).to.have.members([ + '"""\n begin\n \\{', + "alpha", + "}\n mid\n \\{", + "beta", + '}\n end\n """' + ]); + }); +}); diff --git a/packages/java-parser/test/test-block-spec.js b/packages/java-parser/test/test-block-spec.js index ae03566d..f994b479 100644 --- a/packages/java-parser/test/test-block-spec.js +++ b/packages/java-parser/test/test-block-spec.js @@ -64,7 +64,7 @@ describe("The Java Parser should parse TextBlocks", () => { expect(() => javaParser.parse(input, "literal")).to.throw(); }); - it("should parse a multiline 'TextBlock' with ", () => { + it("should parse a multiline 'TextBlock' with \"", () => { const input = `""" one word @@ -86,18 +86,29 @@ describe("The Java Parser should parse TextBlocks", () => { expect(() => javaParser.parse(input, "literal")).to.throw(); }); - it("should not parse a 'TextBlock' ending with 4x\"", () => { + it("should not parse a 'TextBlock' ending with 2x\"", () => { const input = `""" one word - "sentence""" + "sentence" ""`; expect(() => javaParser.parse(input, "literal")).to.throw(); }); - it("should not parse a 'TextBlock' ending with 4x\"", () => { + it("should not parse a 'TextBlock' with 3x\"", () => { + const input = `""" + one word + + "sentence""" + + """`; + + expect(() => javaParser.parse(input, "literal")).to.throw(); + }); + + it("should parse a 'TextBlock' with 3x\" if first is escaped", () => { const input = `""" my text diff --git a/packages/prettier-plugin-java/src/options.js b/packages/prettier-plugin-java/src/options.js index 0822dd65..692093ca 100644 --- a/packages/prettier-plugin-java/src/options.js +++ b/packages/prettier-plugin-java/src/options.js @@ -160,6 +160,11 @@ export default { { value: "classLiteralSuffix" }, { value: "arrayAccessSuffix" }, { value: "methodReferenceSuffix" }, + { value: "templateArgument" }, + { value: "template" }, + { value: "stringTemplate" }, + { value: "textBlockTemplate" }, + { value: "embeddedExpression" }, { value: "pattern" }, { value: "typePattern" }, { value: "recordPattern" }, diff --git a/packages/prettier-plugin-java/src/printers/expressions.ts b/packages/prettier-plugin-java/src/printers/expressions.ts index 25ded33b..b1eba29e 100644 --- a/packages/prettier-plugin-java/src/printers/expressions.ts +++ b/packages/prettier-plugin-java/src/printers/expressions.ts @@ -13,6 +13,7 @@ import { DiamondCtx, DimExprCtx, DimExprsCtx, + EmbeddedExpressionCtx, ExplicitLambdaParameterListCtx, ExpressionCtx, FqnOrRefTypeCtx, @@ -41,7 +42,11 @@ import { RecordPatternCtx, ReferenceTypeCastExpressionCtx, RegularLambdaParameterCtx, + StringTemplateCtx, + TemplateArgumentCtx, + TemplateCtx, TernaryExpressionCtx, + TextBlockTemplateCtx, TypeArgumentsOrDiamondCtx, TypePatternCtx, UnaryExpressionCtx, @@ -420,13 +425,10 @@ export class ExpressionsPrettierVisitor extends BaseCstPrettierPrinter { return rejectAndConcat([ctx.Dot[0], typeArguments, ctx.Identifier[0]]); } - const unqualifiedClassInstanceCreationExpression = this.visit( - ctx.unqualifiedClassInstanceCreationExpression + const suffix = this.visit( + ctx.unqualifiedClassInstanceCreationExpression ?? ctx.templateArgument ); - return rejectAndConcat([ - ctx.Dot[0], - unqualifiedClassInstanceCreationExpression - ]); + return rejectAndConcat([ctx.Dot[0], suffix]); } return this.visitSingle(ctx, params); } @@ -716,6 +718,38 @@ export class ExpressionsPrettierVisitor extends BaseCstPrettierPrinter { return rejectAndConcat([ctx.ColonColon[0], typeArguments, identifierOrNew]); } + templateArgument(ctx: TemplateArgumentCtx) { + return ctx.template + ? this.visit(ctx.template) + : printTokenWithComments((ctx.StringLiteral ?? ctx.TextBlock)![0]); + } + + template(ctx: TemplateCtx) { + return this.visitSingle(ctx); + } + + stringTemplate(ctx: StringTemplateCtx) { + const embeddedExpressions = this.mapVisit(ctx.embeddedExpression); + return concat([ + ctx.StringTemplateBegin[0], + rejectAndJoinSeps(ctx.StringTemplateMid, embeddedExpressions), + ctx.StringTemplateEnd[0] + ]); + } + + textBlockTemplate(ctx: TextBlockTemplateCtx) { + const embeddedExpressions = this.mapVisit(ctx.embeddedExpression); + return concat([ + ctx.TextBlockTemplateBegin[0], + rejectAndJoinSeps(ctx.TextBlockTemplateMid, embeddedExpressions), + ctx.TextBlockTemplateEnd[0] + ]); + } + + embeddedExpression(ctx: EmbeddedExpressionCtx) { + return this.visit(ctx.expression); + } + pattern(ctx: PatternCtx) { return this.visitSingle(ctx); } diff --git a/packages/prettier-plugin-java/test/unit-test/template-expression/_input.java b/packages/prettier-plugin-java/test/unit-test/template-expression/_input.java new file mode 100644 index 00000000..fb4e170e --- /dev/null +++ b/packages/prettier-plugin-java/test/unit-test/template-expression/_input.java @@ -0,0 +1,50 @@ +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.""" + + + \{title} + + +

\{text}

+ + + """; + + 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}"; +} diff --git a/packages/prettier-plugin-java/test/unit-test/template-expression/_output.java b/packages/prettier-plugin-java/test/unit-test/template-expression/_output.java new file mode 100644 index 00000000..71367565 --- /dev/null +++ b/packages/prettier-plugin-java/test/unit-test/template-expression/_output.java @@ -0,0 +1,60 @@ +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.""" + + + \{title} + + +

\{text}

+ + + """; + + 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}"; +} diff --git a/packages/prettier-plugin-java/test/unit-test/template-expression/template-expression-spec.ts b/packages/prettier-plugin-java/test/unit-test/template-expression/template-expression-spec.ts new file mode 100644 index 00000000..0afe96d9 --- /dev/null +++ b/packages/prettier-plugin-java/test/unit-test/template-expression/template-expression-spec.ts @@ -0,0 +1,9 @@ +import path from "path"; +import url from "url"; +import { testSample } from "../../test-utils.js"; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +describe("prettier-java: template expression", () => { + testSample(__dirname); +});