Skip to content

Commit 9b6e751

Browse files
committed
feat: align pattern matching with Java 21 spec
closes #605 closes #610
1 parent a7f9a5f commit 9b6e751

File tree

11 files changed

+223
-88
lines changed

11 files changed

+223
-88
lines changed

packages/java-parser/api.d.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,10 @@ export abstract class JavaCstVisitor<IN, OUT> implements ICstVisitor<IN, OUT> {
375375
arrayAccessSuffix(ctx: ArrayAccessSuffixCtx, param?: IN): OUT;
376376
methodReferenceSuffix(ctx: MethodReferenceSuffixCtx, param?: IN): OUT;
377377
pattern(ctx: PatternCtx, param?: IN): OUT;
378-
primaryPattern(ctx: PrimaryPatternCtx, param?: IN): OUT;
379378
typePattern(ctx: TypePatternCtx, param?: IN): OUT;
379+
recordPattern(ctx: RecordPatternCtx, param?: IN): OUT;
380+
patternList(ctx: PatternListCtx, param?: IN): OUT;
381+
guard(ctx: GuardCtx, param?: IN): OUT;
380382
identifyNewExpressionType(ctx: IdentifyNewExpressionTypeCtx, param?: IN): OUT;
381383
isLambdaExpression(ctx: IsLambdaExpressionCtx, param?: IN): OUT;
382384
isCastExpression(ctx: IsCastExpressionCtx, param?: IN): OUT;
@@ -731,8 +733,10 @@ export abstract class JavaCstVisitorWithDefaults<IN, OUT>
731733
arrayAccessSuffix(ctx: ArrayAccessSuffixCtx, param?: IN): OUT;
732734
methodReferenceSuffix(ctx: MethodReferenceSuffixCtx, param?: IN): OUT;
733735
pattern(ctx: PatternCtx, param?: IN): OUT;
734-
primaryPattern(ctx: PrimaryPatternCtx, param?: IN): OUT;
735736
typePattern(ctx: TypePatternCtx, param?: IN): OUT;
737+
recordPattern(ctx: RecordPatternCtx, param?: IN): OUT;
738+
patternList(ctx: PatternListCtx, param?: IN): OUT;
739+
guard(ctx: GuardCtx, param?: IN): OUT;
736740
identifyNewExpressionType(ctx: IdentifyNewExpressionTypeCtx, param?: IN): OUT;
737741
isLambdaExpression(ctx: IsLambdaExpressionCtx, param?: IN): OUT;
738742
isCastExpression(ctx: IsCastExpressionCtx, param?: IN): OUT;
@@ -2663,6 +2667,7 @@ export type CaseLabelElementCtx = {
26632667
Null?: IToken[];
26642668
Default?: IToken[];
26652669
pattern?: PatternCstNode[];
2670+
guard?: GuardCstNode[];
26662671
caseConstant?: CaseConstantCstNode[];
26672672
};
26682673

@@ -3497,21 +3502,8 @@ export interface PatternCstNode extends CstNode {
34973502
}
34983503

34993504
export type PatternCtx = {
3500-
primaryPattern: PrimaryPatternCstNode[];
3501-
AndAnd?: IToken[];
3502-
binaryExpression?: BinaryExpressionCstNode[];
3503-
};
3504-
3505-
export interface PrimaryPatternCstNode extends CstNode {
3506-
name: "primaryPattern";
3507-
children: PrimaryPatternCtx;
3508-
}
3509-
3510-
export type PrimaryPatternCtx = {
3511-
LBrace?: IToken[];
3512-
pattern?: PatternCstNode[];
3513-
RBrace?: IToken[];
35143505
typePattern?: TypePatternCstNode[];
3506+
recordPattern?: RecordPatternCstNode[];
35153507
};
35163508

35173509
export interface TypePatternCstNode extends CstNode {
@@ -3523,6 +3515,38 @@ export type TypePatternCtx = {
35233515
localVariableDeclaration: LocalVariableDeclarationCstNode[];
35243516
};
35253517

3518+
export interface RecordPatternCstNode extends CstNode {
3519+
name: "recordPattern";
3520+
children: RecordPatternCtx;
3521+
}
3522+
3523+
export type RecordPatternCtx = {
3524+
referenceType: ReferenceTypeCstNode[];
3525+
LBrace: IToken[];
3526+
patternList?: PatternListCstNode[];
3527+
RBrace: IToken[];
3528+
};
3529+
3530+
export interface PatternListCstNode extends CstNode {
3531+
name: "patternList";
3532+
children: PatternListCtx;
3533+
}
3534+
3535+
export type PatternListCtx = {
3536+
pattern: PatternCstNode[];
3537+
Comma?: IToken[];
3538+
};
3539+
3540+
export interface GuardCstNode extends CstNode {
3541+
name: "guard";
3542+
children: GuardCtx;
3543+
}
3544+
3545+
export type GuardCtx = {
3546+
When: IToken[];
3547+
expression: ExpressionCstNode[];
3548+
};
3549+
35263550
export interface IdentifyNewExpressionTypeCstNode extends CstNode {
35273551
name: "identifyNewExpressionType";
35283552
children: IdentifyNewExpressionTypeCtx;

packages/java-parser/src/productions/blocks-and-statements.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,12 @@ function defineRules($, t) {
246246
{ ALT: () => $.CONSUME(t.Default) },
247247
{
248248
GATE: () => this.BACKTRACK_LOOKAHEAD($.pattern),
249-
ALT: () => $.SUBRULE($.pattern)
249+
ALT: () => {
250+
$.SUBRULE($.pattern);
251+
$.OPTION(() => {
252+
$.SUBRULE($.guard);
253+
});
254+
}
250255
},
251256
{
252257
GATE: () => tokenMatcher($.LA(1).tokenType, t.Null) === false,

packages/java-parser/src/productions/classes.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,13 @@ function defineRules($, t) {
202202
// https://docs.oracle.com/javase/specs/jls/se16/html/jls-8.html#jls-VariableDeclaratorList
203203
$.RULE("variableDeclaratorList", () => {
204204
$.SUBRULE($.variableDeclarator);
205-
$.MANY(() => {
206-
$.CONSUME(t.Comma);
207-
$.SUBRULE2($.variableDeclarator);
205+
$.MANY({
206+
// required to distinguish from patternList
207+
GATE: () => !tokenMatcher(this.LA(3).tokenType, t.Identifier),
208+
DEF: () => {
209+
$.CONSUME(t.Comma);
210+
$.SUBRULE2($.variableDeclarator);
211+
}
208212
});
209213
});
210214

packages/java-parser/src/productions/expressions.js

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -561,33 +561,49 @@ function defineRules($, t) {
561561
]);
562562
});
563563

564+
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-Pattern
564565
$.RULE("pattern", () => {
565-
$.SUBRULE($.primaryPattern);
566-
$.OPTION(() => {
567-
$.CONSUME(t.AndAnd);
568-
$.SUBRULE($.binaryExpression);
569-
});
570-
});
571-
572-
$.RULE("primaryPattern", () => {
573566
$.OR([
574567
{
575-
ALT: () => {
576-
$.CONSUME(t.LBrace);
577-
$.SUBRULE($.pattern);
578-
$.CONSUME(t.RBrace);
579-
}
568+
GATE: () => this.BACKTRACK_LOOKAHEAD($.typePattern),
569+
ALT: () => $.SUBRULE($.typePattern)
580570
},
581571
{
582-
ALT: () => $.SUBRULE($.typePattern)
572+
ALT: () => $.SUBRULE($.recordPattern)
583573
}
584574
]);
585575
});
586576

577+
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-TypePattern
587578
$.RULE("typePattern", () => {
588579
$.SUBRULE($.localVariableDeclaration);
589580
});
590581

582+
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-RecordPattern
583+
$.RULE("recordPattern", () => {
584+
$.SUBRULE($.referenceType);
585+
$.CONSUME(t.LBrace);
586+
$.OPTION(() => {
587+
$.SUBRULE($.patternList);
588+
});
589+
$.CONSUME(t.RBrace);
590+
});
591+
592+
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-PatternList
593+
$.RULE("patternList", () => {
594+
$.SUBRULE($.pattern);
595+
$.MANY(() => {
596+
$.CONSUME(t.Comma);
597+
$.SUBRULE2($.pattern);
598+
});
599+
});
600+
601+
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-Guard
602+
$.RULE("guard", () => {
603+
$.CONSUME(t.When);
604+
$.SUBRULE($.expression);
605+
});
606+
591607
// backtracking lookahead logic
592608
$.RULE("identifyNewExpressionType", () => {
593609
$.CONSUME(t.New);

packages/java-parser/src/tokens.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ createToken({
237237
pattern: MAKE_PATTERN('"(?:[^\\\\"]|{{StringCharacter}})*"')
238238
});
239239

240-
// https://docs.oracle.com/javase/specs/jls/se11/html/jls-3.html#jls-3.9
240+
// https://docs.oracle.com/javase/specs/jls/se21/html/jls-3.html#jls-3.9
241241
// TODO: how to handle the special rule (see spec above) for "requires" and "transitive"
242242
const restrictedKeywords = [
243243
"open",
@@ -249,6 +249,7 @@ const restrictedKeywords = [
249249
"to",
250250
"uses",
251251
"provides",
252+
"when",
252253
"with",
253254
"sealed",
254255
"non-sealed",

packages/java-parser/test/pattern-matching/pattern-matching-spec.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@ describe("Pattern matching", () => {
4141
public boolean hasBestOffer(Buyer other) {
4242
return switch (other) {
4343
case null -> true;
44-
case Buyer b && this.bestPrice > b.bestPrice -> true;
44+
case Buyer b when this.bestPrice > b.bestPrice -> true;
4545
default -> false;
4646
};
4747
}
4848
}
4949
`;
5050
expect(() => javaParser.parse(input, "compilationUnit")).to.not.throw();
5151
});
52+
53+
it("should parse pattern list", () => {
54+
const input = `A a, B b`;
55+
expect(() => javaParser.parse(input, "patternList")).to.not.throw();
56+
});
5257
});

packages/prettier-plugin-java/src/options.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,10 @@ module.exports = {
172172
{ value: "arrayAccessSuffix" },
173173
{ value: "methodReferenceSuffix" },
174174
{ value: "pattern" },
175-
{ value: "primaryPattern" },
176175
{ value: "typePattern" },
176+
{ value: "recordPattern" },
177+
{ value: "patternList" },
178+
{ value: "guard" },
177179
{ value: "identifyNewExpressionType" },
178180
{ value: "isLambdaExpression" },
179181
{ value: "isCastExpression" },

packages/prettier-plugin-java/src/printers/blocks-and-statements.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,12 @@ export class BlocksAndStatementPrettierVisitor extends BaseCstPrettierPrinter {
316316
caseLabelElement(ctx: CaseLabelElementCtx) {
317317
if (ctx.Default || ctx.Null) {
318318
return this.getSingle(ctx);
319+
} else if (ctx.pattern) {
320+
const pattern = this.visit(ctx.pattern);
321+
const guard = this.visit(ctx.guard);
322+
return rejectAndJoin(" ", [dedent(pattern), guard]);
319323
}
320-
return this.visitSingle(ctx);
324+
return this.visit(ctx.caseConstant);
321325
}
322326

323327
switchRule(ctx: SwitchRuleCtx) {
@@ -332,7 +336,7 @@ export class BlocksAndStatementPrettierVisitor extends BaseCstPrettierPrinter {
332336
caseInstruction = concat([this.visit(ctx.expression), ctx.Semicolon![0]]);
333337
}
334338

335-
return join(" ", [switchLabel, ctx.Arrow[0], caseInstruction]);
339+
return concat([switchLabel, " ", ctx.Arrow[0], " ", caseInstruction]);
336340
}
337341

338342
caseConstant(ctx: CaseConstantCtx) {

packages/prettier-plugin-java/src/printers/expressions.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
FqnOrRefTypePartCommonCtx,
2020
FqnOrRefTypePartFirstCtx,
2121
FqnOrRefTypePartRestCtx,
22+
GuardCtx,
2223
InferredLambdaParameterListCtx,
2324
IToken,
2425
LambdaBodyCtx,
@@ -33,11 +34,12 @@ import {
3334
NewExpressionCtx,
3435
ParenthesisExpressionCtx,
3536
PatternCtx,
37+
PatternListCtx,
3638
PrimaryCtx,
37-
PrimaryPatternCtx,
3839
PrimaryPrefixCtx,
3940
PrimarySuffixCtx,
4041
PrimitiveCastExpressionCtx,
42+
RecordPatternCtx,
4143
ReferenceTypeCastExpressionCtx,
4244
RegularLambdaParameterCtx,
4345
TernaryExpressionCtx,
@@ -715,32 +717,34 @@ export class ExpressionsPrettierVisitor extends BaseCstPrettierPrinter {
715717
}
716718

717719
pattern(ctx: PatternCtx) {
718-
const primaryPattern = this.visit(ctx.primaryPattern);
719-
if (ctx.AndAnd === undefined) {
720-
return primaryPattern;
721-
}
720+
return this.visitSingle(ctx);
721+
}
722722

723-
const binaryExpression = this.visit(ctx.binaryExpression);
724-
return rejectAndConcat([
725-
primaryPattern,
726-
" ",
727-
ctx.AndAnd[0],
728-
line,
729-
binaryExpression
730-
]);
723+
typePattern(ctx: TypePatternCtx) {
724+
return this.visitSingle(ctx);
731725
}
732726

733-
primaryPattern(ctx: PrimaryPatternCtx) {
734-
if (ctx.LBrace === undefined) {
735-
return this.visitSingle(ctx);
736-
}
727+
recordPattern(ctx: RecordPatternCtx) {
728+
const referenceType = this.visit(ctx.referenceType);
729+
const patternList = this.visit(ctx.patternList);
730+
return concat([
731+
referenceType,
732+
putIntoBraces(patternList, softline, ctx.LBrace[0], ctx.RBrace[0])
733+
]);
734+
}
737735

738-
const pattern = this.visit(ctx.pattern);
739-
return putIntoBraces(pattern, softline, ctx.LBrace[0], ctx.RBrace![0]);
736+
patternList(ctx: PatternListCtx) {
737+
const patterns = this.mapVisit(ctx.pattern);
738+
const commas = ctx.Comma?.map(elt => concat([elt, line])) ?? [];
739+
return rejectAndJoinSeps(commas, patterns);
740740
}
741741

742-
typePattern(ctx: TypePatternCtx) {
743-
return this.visitSingle(ctx);
742+
guard(ctx: GuardCtx) {
743+
const expression = this.visit(ctx.expression, {
744+
addParenthesisToWrapStatement: true
745+
});
746+
747+
return concat([ctx.When[0], " ", dedent(expression)]);
744748
}
745749

746750
identifyNewExpressionType() {

packages/prettier-plugin-java/test/unit-test/pattern-matching/_input.java

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,45 @@ static String formatter(Object o) {
1313
return formatted;
1414
}
1515

16+
public boolean test(final Object obj) {
17+
return obj instanceof final Integer x && (x == 5 || x == 6 || x == 7 || x == 8 || x == 9 || x == 10 || x == 11);
18+
}
19+
1620
void test(Buyer other) {
1721
return switch (other) {
1822
case null -> true;
19-
case Buyer b && this.bestPrice > b.bestPrice -> true;
20-
case Buyer b && this.bestPrice > b.bestPrice -> {
23+
case Buyer b when this.bestPrice > b.bestPrice -> true;
24+
case Buyer b when this.bestPrice > b.bestPrice -> {
2125
return true;
2226
}
23-
case (Buyer b && this.bestPrice > b.bestPrice) -> true;
24-
case Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> true;
25-
case Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> {
27+
case Buyer titi when this.bestPriceaaaaaaaazzzzzaaaaaaaaaq > b.bestPrice -> true;
28+
case Buyer titi when this.bestPriceaaaaaazzzaaaaaaaaaq > b.bestPrice -> true;
29+
case Buyer b when this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> true;
30+
case Buyer b when this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice -> {
2631
return true;
2732
}
28-
case (Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice) -> true;
29-
case (Buyer b && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice && this.bestPrice > b.bestPrice) -> {
33+
case Buyer b when (
34+
this.bestPrice > b.bestPrice &&
35+
this.bestPrice > b.bestPrice &&
36+
this.bestPrice > b.bestPrice &&
37+
this.bestPrice > b.bestPrice
38+
) -> {
3039
return true;
3140
}
3241
default -> false;
3342
};
3443
}
44+
45+
int recordPatterns(MyRecord r) {
46+
return switch (r) {
47+
case null, default -> 0;
48+
case MyRecord(A a) -> 0;
49+
case MyRecord(A a, B b) -> 0;
50+
case MyRecord(MyRecord(A a), B b) -> 0;
51+
case MyRecord(MyLongRecordTypeName(LongTypeName longVariableName, LongTypeName longVariableName), MyLongRecordTypeName(LongTypeName longVariableName, LongTypeName longVariableName)) -> 0;
52+
case MyRecord(LongTypeName longVariableName, LongTypeName longVariableName) -> 0;
53+
case MyRecord(LongTypeName longVariableName, LongTypeName longVariableName) when this.longVariableName > longVariableName && this.longVariableName > longVariableName -> 0;
54+
case MyRecord(LongTypeName longVariableName, LongTypeName longVariableName) when this.longVariableName > longVariableName && this.longVariableName > longVariableName -> longMethodName(longVariableName, longVariableName, longVariableName, longVariableName);
55+
};
56+
}
3557
}

0 commit comments

Comments
 (0)