From 9b6e75163269c60b6eae49709b1f09cd1aad11c3 Mon Sep 17 00:00:00 2001 From: Jordan Kiesel Date: Sun, 29 Oct 2023 11:57:28 -0600 Subject: [PATCH] feat: align pattern matching with Java 21 spec closes #605 closes #610 --- packages/java-parser/api.d.ts | 56 ++++++++---- .../src/productions/blocks-and-statements.js | 7 +- .../java-parser/src/productions/classes.js | 10 ++- .../src/productions/expressions.js | 44 ++++++--- packages/java-parser/src/tokens.js | 3 +- .../pattern-matching/pattern-matching-spec.js | 7 +- packages/prettier-plugin-java/src/options.js | 4 +- .../src/printers/blocks-and-statements.ts | 8 +- .../src/printers/expressions.ts | 46 +++++----- .../unit-test/pattern-matching/_input.java | 36 ++++++-- .../unit-test/pattern-matching/_output.java | 90 ++++++++++++++----- 11 files changed, 223 insertions(+), 88 deletions(-) diff --git a/packages/java-parser/api.d.ts b/packages/java-parser/api.d.ts index f94a263c..0ebce167 100644 --- a/packages/java-parser/api.d.ts +++ b/packages/java-parser/api.d.ts @@ -375,8 +375,10 @@ export abstract class JavaCstVisitor implements ICstVisitor { arrayAccessSuffix(ctx: ArrayAccessSuffixCtx, param?: IN): OUT; methodReferenceSuffix(ctx: MethodReferenceSuffixCtx, param?: IN): OUT; pattern(ctx: PatternCtx, param?: IN): OUT; - primaryPattern(ctx: PrimaryPatternCtx, param?: IN): OUT; typePattern(ctx: TypePatternCtx, param?: IN): OUT; + recordPattern(ctx: RecordPatternCtx, param?: IN): OUT; + patternList(ctx: PatternListCtx, param?: IN): OUT; + guard(ctx: GuardCtx, param?: IN): OUT; identifyNewExpressionType(ctx: IdentifyNewExpressionTypeCtx, param?: IN): OUT; isLambdaExpression(ctx: IsLambdaExpressionCtx, param?: IN): OUT; isCastExpression(ctx: IsCastExpressionCtx, param?: IN): OUT; @@ -731,8 +733,10 @@ export abstract class JavaCstVisitorWithDefaults arrayAccessSuffix(ctx: ArrayAccessSuffixCtx, param?: IN): OUT; methodReferenceSuffix(ctx: MethodReferenceSuffixCtx, param?: IN): OUT; pattern(ctx: PatternCtx, param?: IN): OUT; - primaryPattern(ctx: PrimaryPatternCtx, param?: IN): OUT; typePattern(ctx: TypePatternCtx, param?: IN): OUT; + recordPattern(ctx: RecordPatternCtx, param?: IN): OUT; + patternList(ctx: PatternListCtx, param?: IN): OUT; + guard(ctx: GuardCtx, param?: IN): OUT; identifyNewExpressionType(ctx: IdentifyNewExpressionTypeCtx, param?: IN): OUT; isLambdaExpression(ctx: IsLambdaExpressionCtx, param?: IN): OUT; isCastExpression(ctx: IsCastExpressionCtx, param?: IN): OUT; @@ -2663,6 +2667,7 @@ export type CaseLabelElementCtx = { Null?: IToken[]; Default?: IToken[]; pattern?: PatternCstNode[]; + guard?: GuardCstNode[]; caseConstant?: CaseConstantCstNode[]; }; @@ -3497,21 +3502,8 @@ export interface PatternCstNode extends CstNode { } export type PatternCtx = { - primaryPattern: PrimaryPatternCstNode[]; - AndAnd?: IToken[]; - binaryExpression?: BinaryExpressionCstNode[]; -}; - -export interface PrimaryPatternCstNode extends CstNode { - name: "primaryPattern"; - children: PrimaryPatternCtx; -} - -export type PrimaryPatternCtx = { - LBrace?: IToken[]; - pattern?: PatternCstNode[]; - RBrace?: IToken[]; typePattern?: TypePatternCstNode[]; + recordPattern?: RecordPatternCstNode[]; }; export interface TypePatternCstNode extends CstNode { @@ -3523,6 +3515,38 @@ export type TypePatternCtx = { localVariableDeclaration: LocalVariableDeclarationCstNode[]; }; +export interface RecordPatternCstNode extends CstNode { + name: "recordPattern"; + children: RecordPatternCtx; +} + +export type RecordPatternCtx = { + referenceType: ReferenceTypeCstNode[]; + LBrace: IToken[]; + patternList?: PatternListCstNode[]; + RBrace: IToken[]; +}; + +export interface PatternListCstNode extends CstNode { + name: "patternList"; + children: PatternListCtx; +} + +export type PatternListCtx = { + pattern: PatternCstNode[]; + Comma?: IToken[]; +}; + +export interface GuardCstNode extends CstNode { + name: "guard"; + children: GuardCtx; +} + +export type GuardCtx = { + When: IToken[]; + expression: ExpressionCstNode[]; +}; + export interface IdentifyNewExpressionTypeCstNode extends CstNode { name: "identifyNewExpressionType"; children: IdentifyNewExpressionTypeCtx; diff --git a/packages/java-parser/src/productions/blocks-and-statements.js b/packages/java-parser/src/productions/blocks-and-statements.js index d19f00b9..7f72bea1 100644 --- a/packages/java-parser/src/productions/blocks-and-statements.js +++ b/packages/java-parser/src/productions/blocks-and-statements.js @@ -246,7 +246,12 @@ function defineRules($, t) { { ALT: () => $.CONSUME(t.Default) }, { GATE: () => this.BACKTRACK_LOOKAHEAD($.pattern), - ALT: () => $.SUBRULE($.pattern) + ALT: () => { + $.SUBRULE($.pattern); + $.OPTION(() => { + $.SUBRULE($.guard); + }); + } }, { GATE: () => tokenMatcher($.LA(1).tokenType, t.Null) === false, diff --git a/packages/java-parser/src/productions/classes.js b/packages/java-parser/src/productions/classes.js index 509b195d..00b3d17d 100644 --- a/packages/java-parser/src/productions/classes.js +++ b/packages/java-parser/src/productions/classes.js @@ -202,9 +202,13 @@ function defineRules($, t) { // https://docs.oracle.com/javase/specs/jls/se16/html/jls-8.html#jls-VariableDeclaratorList $.RULE("variableDeclaratorList", () => { $.SUBRULE($.variableDeclarator); - $.MANY(() => { - $.CONSUME(t.Comma); - $.SUBRULE2($.variableDeclarator); + $.MANY({ + // required to distinguish from patternList + GATE: () => !tokenMatcher(this.LA(3).tokenType, t.Identifier), + DEF: () => { + $.CONSUME(t.Comma); + $.SUBRULE2($.variableDeclarator); + } }); }); diff --git a/packages/java-parser/src/productions/expressions.js b/packages/java-parser/src/productions/expressions.js index 36ba7f52..de43c305 100644 --- a/packages/java-parser/src/productions/expressions.js +++ b/packages/java-parser/src/productions/expressions.js @@ -561,33 +561,49 @@ function defineRules($, t) { ]); }); + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-Pattern $.RULE("pattern", () => { - $.SUBRULE($.primaryPattern); - $.OPTION(() => { - $.CONSUME(t.AndAnd); - $.SUBRULE($.binaryExpression); - }); - }); - - $.RULE("primaryPattern", () => { $.OR([ { - ALT: () => { - $.CONSUME(t.LBrace); - $.SUBRULE($.pattern); - $.CONSUME(t.RBrace); - } + GATE: () => this.BACKTRACK_LOOKAHEAD($.typePattern), + ALT: () => $.SUBRULE($.typePattern) }, { - ALT: () => $.SUBRULE($.typePattern) + ALT: () => $.SUBRULE($.recordPattern) } ]); }); + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-TypePattern $.RULE("typePattern", () => { $.SUBRULE($.localVariableDeclaration); }); + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-RecordPattern + $.RULE("recordPattern", () => { + $.SUBRULE($.referenceType); + $.CONSUME(t.LBrace); + $.OPTION(() => { + $.SUBRULE($.patternList); + }); + $.CONSUME(t.RBrace); + }); + + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-PatternList + $.RULE("patternList", () => { + $.SUBRULE($.pattern); + $.MANY(() => { + $.CONSUME(t.Comma); + $.SUBRULE2($.pattern); + }); + }); + + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-Guard + $.RULE("guard", () => { + $.CONSUME(t.When); + $.SUBRULE($.expression); + }); + // backtracking lookahead logic $.RULE("identifyNewExpressionType", () => { $.CONSUME(t.New); diff --git a/packages/java-parser/src/tokens.js b/packages/java-parser/src/tokens.js index 89a10e8b..8102b8da 100644 --- a/packages/java-parser/src/tokens.js +++ b/packages/java-parser/src/tokens.js @@ -237,7 +237,7 @@ createToken({ pattern: MAKE_PATTERN('"(?:[^\\\\"]|{{StringCharacter}})*"') }); -// https://docs.oracle.com/javase/specs/jls/se11/html/jls-3.html#jls-3.9 +// 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 = [ "open", @@ -249,6 +249,7 @@ const restrictedKeywords = [ "to", "uses", "provides", + "when", "with", "sealed", "non-sealed", diff --git a/packages/java-parser/test/pattern-matching/pattern-matching-spec.js b/packages/java-parser/test/pattern-matching/pattern-matching-spec.js index e9107a19..a92d5c1b 100644 --- a/packages/java-parser/test/pattern-matching/pattern-matching-spec.js +++ b/packages/java-parser/test/pattern-matching/pattern-matching-spec.js @@ -41,7 +41,7 @@ describe("Pattern matching", () => { public boolean hasBestOffer(Buyer other) { return switch (other) { case null -> true; - case Buyer b && this.bestPrice > b.bestPrice -> true; + case Buyer b when this.bestPrice > b.bestPrice -> true; default -> false; }; } @@ -49,4 +49,9 @@ describe("Pattern matching", () => { `; expect(() => javaParser.parse(input, "compilationUnit")).to.not.throw(); }); + + it("should parse pattern list", () => { + const input = `A a, B b`; + expect(() => javaParser.parse(input, "patternList")).to.not.throw(); + }); }); diff --git a/packages/prettier-plugin-java/src/options.js b/packages/prettier-plugin-java/src/options.js index 6fcce65e..6cf1862a 100644 --- a/packages/prettier-plugin-java/src/options.js +++ b/packages/prettier-plugin-java/src/options.js @@ -172,8 +172,10 @@ module.exports = { { value: "arrayAccessSuffix" }, { value: "methodReferenceSuffix" }, { value: "pattern" }, - { value: "primaryPattern" }, { value: "typePattern" }, + { value: "recordPattern" }, + { value: "patternList" }, + { value: "guard" }, { value: "identifyNewExpressionType" }, { value: "isLambdaExpression" }, { value: "isCastExpression" }, diff --git a/packages/prettier-plugin-java/src/printers/blocks-and-statements.ts b/packages/prettier-plugin-java/src/printers/blocks-and-statements.ts index 993ad624..0ad4b8e6 100644 --- a/packages/prettier-plugin-java/src/printers/blocks-and-statements.ts +++ b/packages/prettier-plugin-java/src/printers/blocks-and-statements.ts @@ -316,8 +316,12 @@ export class BlocksAndStatementPrettierVisitor extends BaseCstPrettierPrinter { caseLabelElement(ctx: CaseLabelElementCtx) { if (ctx.Default || ctx.Null) { return this.getSingle(ctx); + } else if (ctx.pattern) { + const pattern = this.visit(ctx.pattern); + const guard = this.visit(ctx.guard); + return rejectAndJoin(" ", [dedent(pattern), guard]); } - return this.visitSingle(ctx); + return this.visit(ctx.caseConstant); } switchRule(ctx: SwitchRuleCtx) { @@ -332,7 +336,7 @@ export class BlocksAndStatementPrettierVisitor extends BaseCstPrettierPrinter { caseInstruction = concat([this.visit(ctx.expression), ctx.Semicolon![0]]); } - return join(" ", [switchLabel, ctx.Arrow[0], caseInstruction]); + return concat([switchLabel, " ", ctx.Arrow[0], " ", caseInstruction]); } caseConstant(ctx: CaseConstantCtx) { diff --git a/packages/prettier-plugin-java/src/printers/expressions.ts b/packages/prettier-plugin-java/src/printers/expressions.ts index 4cdb8919..724ac157 100644 --- a/packages/prettier-plugin-java/src/printers/expressions.ts +++ b/packages/prettier-plugin-java/src/printers/expressions.ts @@ -19,6 +19,7 @@ import { FqnOrRefTypePartCommonCtx, FqnOrRefTypePartFirstCtx, FqnOrRefTypePartRestCtx, + GuardCtx, InferredLambdaParameterListCtx, IToken, LambdaBodyCtx, @@ -33,11 +34,12 @@ import { NewExpressionCtx, ParenthesisExpressionCtx, PatternCtx, + PatternListCtx, PrimaryCtx, - PrimaryPatternCtx, PrimaryPrefixCtx, PrimarySuffixCtx, PrimitiveCastExpressionCtx, + RecordPatternCtx, ReferenceTypeCastExpressionCtx, RegularLambdaParameterCtx, TernaryExpressionCtx, @@ -715,32 +717,34 @@ export class ExpressionsPrettierVisitor extends BaseCstPrettierPrinter { } pattern(ctx: PatternCtx) { - const primaryPattern = this.visit(ctx.primaryPattern); - if (ctx.AndAnd === undefined) { - return primaryPattern; - } + return this.visitSingle(ctx); + } - const binaryExpression = this.visit(ctx.binaryExpression); - return rejectAndConcat([ - primaryPattern, - " ", - ctx.AndAnd[0], - line, - binaryExpression - ]); + typePattern(ctx: TypePatternCtx) { + return this.visitSingle(ctx); } - primaryPattern(ctx: PrimaryPatternCtx) { - if (ctx.LBrace === undefined) { - return this.visitSingle(ctx); - } + recordPattern(ctx: RecordPatternCtx) { + const referenceType = this.visit(ctx.referenceType); + const patternList = this.visit(ctx.patternList); + return concat([ + referenceType, + putIntoBraces(patternList, softline, ctx.LBrace[0], ctx.RBrace[0]) + ]); + } - const pattern = this.visit(ctx.pattern); - return putIntoBraces(pattern, softline, ctx.LBrace[0], ctx.RBrace![0]); + patternList(ctx: PatternListCtx) { + const patterns = this.mapVisit(ctx.pattern); + const commas = ctx.Comma?.map(elt => concat([elt, line])) ?? []; + return rejectAndJoinSeps(commas, patterns); } - typePattern(ctx: TypePatternCtx) { - return this.visitSingle(ctx); + guard(ctx: GuardCtx) { + const expression = this.visit(ctx.expression, { + addParenthesisToWrapStatement: true + }); + + return concat([ctx.When[0], " ", dedent(expression)]); } identifyNewExpressionType() { diff --git a/packages/prettier-plugin-java/test/unit-test/pattern-matching/_input.java b/packages/prettier-plugin-java/test/unit-test/pattern-matching/_input.java index be875b50..000023a7 100644 --- a/packages/prettier-plugin-java/test/unit-test/pattern-matching/_input.java +++ b/packages/prettier-plugin-java/test/unit-test/pattern-matching/_input.java @@ -13,23 +13,45 @@ static String formatter(Object o) { return formatted; } + public boolean test(final Object obj) { + return obj instanceof final Integer x && (x == 5 || x == 6 || x == 7 || x == 8 || x == 9 || x == 10 || x == 11); + } + void test(Buyer other) { return switch (other) { case null -> true; - case Buyer b && this.bestPrice > b.bestPrice -> true; - case Buyer b && this.bestPrice > b.bestPrice -> { + case Buyer b when this.bestPrice > b.bestPrice -> true; + case Buyer b when this.bestPrice > b.bestPrice -> { return true; } - case (Buyer b && this.bestPrice > b.bestPrice) -> true; - case Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> true; - case Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> { + case Buyer titi when this.bestPriceaaaaaaaazzzzzaaaaaaaaaq > b.bestPrice -> true; + case Buyer titi when this.bestPriceaaaaaazzzaaaaaaaaaq > b.bestPrice -> true; + case Buyer b when this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> true; + case Buyer b when this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> { return true; } - case (Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice) -> true; - case (Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice) -> { + case Buyer b when ( + this.bestPrice > b.bestPrice && + this.bestPrice > b.bestPrice && + this.bestPrice > b.bestPrice && + this.bestPrice > b.bestPrice + ) -> { return true; } default -> false; }; } + + int recordPatterns(MyRecord r) { + return switch (r) { + case null, default -> 0; + case MyRecord(A a) -> 0; + case MyRecord(A a, B b) -> 0; + case MyRecord(MyRecord(A a), B b) -> 0; + case MyRecord(MyLongRecordTypeName(LongTypeName longVariableName, LongTypeName longVariableName), MyLongRecordTypeName(LongTypeName longVariableName, LongTypeName longVariableName)) -> 0; + case MyRecord(LongTypeName longVariableName, LongTypeName longVariableName) -> 0; + case MyRecord(LongTypeName longVariableName, LongTypeName longVariableName) when this.longVariableName > longVariableName && this.longVariableName > longVariableName -> 0; + case MyRecord(LongTypeName longVariableName, LongTypeName longVariableName) when this.longVariableName > longVariableName && this.longVariableName > longVariableName -> longMethodName(longVariableName, longVariableName, longVariableName, longVariableName); + }; + } } diff --git a/packages/prettier-plugin-java/test/unit-test/pattern-matching/_output.java b/packages/prettier-plugin-java/test/unit-test/pattern-matching/_output.java index 8b9f343f..087f5fbb 100644 --- a/packages/prettier-plugin-java/test/unit-test/pattern-matching/_output.java +++ b/packages/prettier-plugin-java/test/unit-test/pattern-matching/_output.java @@ -19,43 +19,91 @@ static String formatter(Object o) { return formatted; } + public boolean test(final Object obj) { + return ( + obj instanceof final Integer x && + (x == 5 || x == 6 || x == 7 || x == 8 || x == 9 || x == 10 || x == 11) + ); + } + void test(Buyer other) { return switch (other) { case null -> true; - case Buyer b && this.bestPrice > b.bestPrice -> true; - case Buyer b && this.bestPrice > b.bestPrice -> { + case Buyer b when this.bestPrice > b.bestPrice -> true; + case Buyer b when this.bestPrice > b.bestPrice -> { return true; } - case (Buyer b && this.bestPrice > b.bestPrice) -> true; - case Buyer b && + case Buyer titi when ( + this.bestPriceaaaaaaaazzzzzaaaaaaaaaq > b.bestPrice + ) -> true; + case Buyer titi when ( + this.bestPriceaaaaaazzzaaaaaaaaaq > b.bestPrice + ) -> true; + case Buyer b when ( this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice -> true; - case Buyer b && + this.bestPrice > b.bestPrice + ) -> true; + case Buyer b when ( this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice -> { + this.bestPrice > b.bestPrice + ) -> { return true; } - case ( - Buyer b && - this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice - ) -> true; - case ( - Buyer b && - this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice && - this.bestPrice > b.bestPrice - ) -> { + case Buyer b when ( + this.bestPrice > b.bestPrice && + this.bestPrice > b.bestPrice && + this.bestPrice > b.bestPrice && + this.bestPrice > b.bestPrice + ) -> { return true; } default -> false; }; } + + int recordPatterns(MyRecord r) { + return switch (r) { + case null, default -> 0; + case MyRecord(A a) -> 0; + case MyRecord(A a, B b) -> 0; + case MyRecord(MyRecord(A a), B b) -> 0; + case MyRecord( + MyLongRecordTypeName( + LongTypeName longVariableName, + LongTypeName longVariableName + ), + MyLongRecordTypeName( + LongTypeName longVariableName, + LongTypeName longVariableName + ) + ) -> 0; + case MyRecord( + LongTypeName longVariableName, + LongTypeName longVariableName + ) -> 0; + case MyRecord( + LongTypeName longVariableName, + LongTypeName longVariableName + ) when ( + this.longVariableName > longVariableName && + this.longVariableName > longVariableName + ) -> 0; + case MyRecord( + LongTypeName longVariableName, + LongTypeName longVariableName + ) when ( + this.longVariableName > longVariableName && + this.longVariableName > longVariableName + ) -> longMethodName( + longVariableName, + longVariableName, + longVariableName, + longVariableName + ); + }; + } }