Skip to content

Commit

Permalink
feat: align pattern matching with Java 21 spec
Browse files Browse the repository at this point in the history
closes #605
closes #610
  • Loading branch information
jtkiesel committed Oct 29, 2023
1 parent a7f9a5f commit 9b6e751
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 88 deletions.
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

0 comments on commit 9b6e751

Please sign in to comment.