Skip to content

feat: align pattern matching with Java 21 spec #611

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions packages/java-parser/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,10 @@ export abstract class JavaCstVisitor<IN, OUT> implements ICstVisitor<IN, OUT> {
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;
Expand Down Expand Up @@ -731,8 +733,10 @@ export abstract class JavaCstVisitorWithDefaults<IN, OUT>
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;
Expand Down Expand Up @@ -2663,6 +2667,7 @@ export type CaseLabelElementCtx = {
Null?: IToken[];
Default?: IToken[];
pattern?: PatternCstNode[];
guard?: GuardCstNode[];
caseConstant?: CaseConstantCstNode[];
};

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions packages/java-parser/src/productions/classes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
});

Expand Down
44 changes: 30 additions & 14 deletions packages/java-parser/src/productions/expressions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion packages/java-parser/src/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -249,6 +249,7 @@ const restrictedKeywords = [
"to",
"uses",
"provides",
"when",
"with",
"sealed",
"non-sealed",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,17 @@ 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;
};
}
}
`;
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();
});
});
4 changes: 3 additions & 1 deletion packages/prettier-plugin-java/src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
46 changes: 25 additions & 21 deletions packages/prettier-plugin-java/src/printers/expressions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
FqnOrRefTypePartCommonCtx,
FqnOrRefTypePartFirstCtx,
FqnOrRefTypePartRestCtx,
GuardCtx,
InferredLambdaParameterListCtx,
IToken,
LambdaBodyCtx,
Expand All @@ -33,11 +34,12 @@ import {
NewExpressionCtx,
ParenthesisExpressionCtx,
PatternCtx,
PatternListCtx,
PrimaryCtx,
PrimaryPatternCtx,
PrimaryPrefixCtx,
PrimarySuffixCtx,
PrimitiveCastExpressionCtx,
RecordPatternCtx,
ReferenceTypeCastExpressionCtx,
RegularLambdaParameterCtx,
TernaryExpressionCtx,
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
}
Loading