diff --git a/src/grammar/lexer.flex b/src/grammar/lexer.flex index ec46dc92..8477a043 100644 --- a/src/grammar/lexer.flex +++ b/src/grammar/lexer.flex @@ -199,7 +199,7 @@ Id = ([:jletter:]|{UnicodeChar}) ([:jletterdigit:]|{UnicodeChar})* JavadocEnd = "*"+ "/" %state JAVADOC JAVADOCTAG JAVADOCLINE CODEBLOCK PARENBLOCK ASSIGNMENT STRING CHAR SINGLELINECOMMENT MULTILINECOMMENT ANNOTATION ANNOSTRING ANNOCHAR ARGUMENTS NAME -%state ANNOTATIONTYPE ENUM MODULE TYPE ANNOTATIONNOARG ATANNOTATION +%state ANNOTATIONTYPE ENUM MODULE RECORD TYPE ANNOTATIONNOARG ATANNOTATION %state NAME_OR_MODIFIER %% @@ -234,7 +234,7 @@ JavadocEnd = "*"+ "/" { {WhiteSpace} { popState(); } } - { + { "." { return Parser.DOT; } "..." { return Parser.DOTDOTDOT; } "," { return Parser.COMMA; } @@ -260,6 +260,7 @@ JavadocEnd = "*"+ "/" "implements" { return Parser.IMPLEMENTS; } "super" { return Parser.SUPER; } "new" { return Parser.NEW; } + "record" { return Parser.RECORD; } "[" { nestingDepth++; return Parser.SQUAREOPEN; } "]" { nestingDepth--; return Parser.SQUARECLOSE; } @@ -421,7 +422,7 @@ JavadocEnd = "*"+ "/" } } } - { + { "default" { return Parser.DEFAULT; } } { @@ -432,7 +433,7 @@ JavadocEnd = "*"+ "/" {Id} / {WhiteSpace}* [;{(] { resetAnnotatedElementLine(); popState(); return Parser.IDENTIFIER; } {Id} { popState(); return Parser.IDENTIFIER; } } - { + { {Id} { return Parser.IDENTIFIER; } } @@ -646,12 +647,12 @@ JavadocEnd = "*"+ "/" } } - { + { "\"" { if (appendingToCodeBody) { codeBody.append('"'); } pushState(STRING); } \' { if (appendingToCodeBody) { codeBody.append('\''); } pushState(CHAR); } } - { + { "//" { if (appendingToCodeBody) { codeBody.append("//"); } pushState(SINGLELINECOMMENT); } "/*" { if (appendingToCodeBody) { codeBody.append("/*"); } pushState(MULTILINECOMMENT); } "/**/" { if (appendingToCodeBody) { codeBody.append("/**/"); } } diff --git a/src/grammar/parser.y b/src/grammar/parser.y index 84fcfd77..5e26a2f8 100644 --- a/src/grammar/parser.y +++ b/src/grammar/parser.y @@ -32,7 +32,7 @@ import java.util.Stack; %token STAREQUALS SLASHEQUALS PERCENTEQUALS PLUSEQUALS MINUSEQUALS LESSTHAN2EQUALS GREATERTHAN2EQUALS GREATERTHAN3EQUALS AMPERSANDEQUALS CIRCUMFLEXEQUALS VERTLINEEQUALS %token PACKAGE IMPORT PUBLIC PROTECTED PRIVATE STATIC FINAL ABSTRACT NATIVE STRICTFP SYNCHRONIZED TRANSIENT VOLATILE DEFAULT %token OPEN MODULE REQUIRES TRANSITIVE EXPORTS OPENS TO USES PROVIDES WITH -%token CLASS INTERFACE ENUM ANNOINTERFACE THROWS EXTENDS IMPLEMENTS SUPER DEFAULT NEW +%token CLASS INTERFACE ENUM RECORD ANNOINTERFACE THROWS EXTENDS IMPLEMENTS SUPER DEFAULT NEW %token BRACEOPEN BRACECLOSE SQUAREOPEN SQUARECLOSE PARENOPEN PARENCLOSE %token LESSTHAN GREATERTHAN LESSEQUALS GREATEREQUALS %token LESSTHAN2 GREATERTHAN2 GREATERTHAN3 @@ -256,12 +256,14 @@ TypeDeclaration: ClassDeclaration // ClassDeclaration: // NormalClassDeclaration // EnumDeclaration +// RecordDeclaration ClassDeclaration: NormalClassDeclaration | EnumDeclaration + | RecordDeclaration ; // NormalClassDeclaration: -// {ClassModifier} class Identifier [TypeParameters] [Superclass] [Superinterfaces] ClassBody +// {ClassModifier} class Identifier [TypeParameters] [Superclass] [ClassImplements] ClassBody NormalClassDeclaration: Modifiers_opt CLASS IDENTIFIER { cls.setType(ClassDef.CLASS); @@ -269,7 +271,7 @@ NormalClassDeclaration: Modifiers_opt CLASS IDENTIFIER cls.getModifiers().addAll(modifiers); modifiers.clear(); cls.setName( $3 ); } - TypeParameters_opt Superclass_opt Superinterfaces_opt + TypeParameters_opt Superclass_opt ClassImplements_opt { cls.setTypeParameters(typeParams); builder.beginClass(cls); @@ -308,9 +310,9 @@ Superclass_opt: } ; -// Superinterfaces: +// ClassImplements: // implements InterfaceTypeList -Superinterfaces_opt: +ClassImplements_opt: | IMPLEMENTS TypeList { cls.getImplements().addAll( typeList ); @@ -609,7 +611,7 @@ ConstructorDeclaration: Modifiers_opt IDENTIFIER // Primary . [TypeArguments] super ( [ArgumentList] ) ; // EnumDeclaration: -// {ClassModifier} enum Identifier [Superinterfaces] EnumBody +// {ClassModifier} enum Identifier [ClassImplements] EnumBody EnumDeclaration: Modifiers_opt ENUM IDENTIFIER { cls.setLineNumber(lexer.getLine()); @@ -620,7 +622,7 @@ EnumDeclaration: Modifiers_opt ENUM IDENTIFIER cls = new ClassDef(); fieldType = new TypeDef($3, 0); } - Superinterfaces_opt EnumBody + ClassImplements_opt EnumBody ; // EnumBody: @@ -669,6 +671,38 @@ EnumBodyDeclarations_opt: | SEMI ClassBodyDeclarations_opt ; +// RecordDeclaration: +// {ClassModifier} record TypeIdentifier [TypeParameters] RecordHeader [ClassImplements] RecordBody +RecordDeclaration: Modifiers_opt RECORD IDENTIFIER TypeParameters_opt RecordHeader ClassImplements_opt RecordBody + +// RecordHeader: +// ( [RecordComponentList] ) +RecordHeader: PARENOPEN RecordComponentList_opt PARENCLOSE + +// RecordComponentList: +// RecordComponent {, RecordComponent} +RecordComponentList: RecordComponentList COMMA RecordComponent + | RecordComponent + ; +RecordComponentList_opt: + | RecordComponentList + ; + +// RecordComponent: +// {RecordComponentModifier} UnannType Identifier +// VariableArityRecordComponent +RecordComponent: Annotations_opt /* ={RecordComponentModifier} */ Type /* =UnannType */ IDENTIFIER + | VariableArityRecordComponent + ; + +// VariableArityRecordComponent: +// {RecordComponentModifier} UnannType {Annotation} ... Identifier +VariableArityRecordComponent: Annotations_opt /* ={RecordComponentModifier} */ Type /* =UnannType */ DOTDOTDOT IDENTIFIER + +// RecordBody: +// { {RecordBodyDeclaration} } +RecordBody: CODEBLOCK + // ----------------------------- // Productions from �9 (Interfaces) // ----------------------------- diff --git a/src/test/java/com/thoughtworks/qdox/RecordsTest.java b/src/test/java/com/thoughtworks/qdox/RecordsTest.java new file mode 100644 index 00000000..9947e1e5 --- /dev/null +++ b/src/test/java/com/thoughtworks/qdox/RecordsTest.java @@ -0,0 +1,141 @@ +package com.thoughtworks.qdox; + +import java.io.StringReader; + +import org.junit.Test; + +/** + * Examples from https://docs.oracle.com/en/java/javase/16/language/records.html + * + * @author Robert Scholte + */ +public class RecordsTest +{ + private JavaProjectBuilder builder = new JavaProjectBuilder(); + + @Test + public void withTwoFields() { + String source = "record Rectangle(double length, double width) { }"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withCanonicalConstructor() { + String source = "record Rectangle(double length, double width) {\n" + + " public Rectangle(double length, double width) {\n" + + " if (length <= 0 || width <= 0) {\n" + + " throw new java.lang.IllegalArgumentException(\n" + + " String.format(\"Invalid dimensions: %f, %f\", length, width));\n" + + " }\n" + + " this.length = length;\n" + + " this.width = width;\n" + + " }\n" + + "}"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withCompactConstructor() { + String source = "record Rectangle(double length, double width) {\n" + + " public Rectangle {\n" + + " if (length <= 0 || width <= 0) {\n" + + " throw new java.lang.IllegalArgumentException(\n" + + " String.format(\"Invalid dimensions: %f, %f\", length, width));\n" + + " }\n" + + " }\n" + + "}"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withPublicAccessorMethod() { + String source = "record Rectangle(double length, double width) {\n" + + " \n" + + " // Public accessor method\n" + + " public double length() {\n" + + " System.out.println(\"Length is \" + length);\n" + + " return length;\n" + + " }\n" + + "}"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withStaticMembers() { + String source = "record Rectangle(double length, double width) {\n" + + " \n" + + " // Static field\n" + + " static double goldenRatio;\n" + + "\n" + + " // Static initializer\n" + + " static {\n" + + " goldenRatio = (1 + Math.sqrt(5)) / 2;\n" + + " }\n" + + "\n" + + " // Static method\n" + + " public static Rectangle createGoldenRectangle(double width) {\n" + + " return new Rectangle(width, width * goldenRatio);\n" + + " }\n" + + "}"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withNonStaticMembers() { + String source = "record Rectangle(double length, double width) {\n" + + "\n" + + " // Field declarations must be static:\n" + + " BiFunction diagonal;\n" + + "\n" + + " // Instance initializers are not allowed in records:\n" + + " {\n" + + " diagonal = (x, y) -> Math.sqrt(x*x + y*y);\n" + + " }\n" + + "}"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withNestedRecord() { + String source = "record Rectangle(double length, double width) {\n" + + "\n" + + " // Nested record class\n" + + " record RotationAngle(double angle) {\n" + + " public RotationAngle {\n" + + " angle = Math.toRadians(angle);\n" + + " }\n" + + " }\n" + + " \n" + + " // Public instance method\n" + + " public Rectangle getRotatedRectangleBoundingBox(double angle) {\n" + + " RotationAngle ra = new RotationAngle(angle);\n" + + " double x = Math.abs(length * Math.cos(ra.angle())) +\n" + + " Math.abs(width * Math.sin(ra.angle()));\n" + + " double y = Math.abs(length * Math.sin(ra.angle())) +\n" + + " Math.abs(width * Math.cos(ra.angle()));\n" + + " return new Rectangle(x, y);\n" + + " }\n" + + "}"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withGenerics() { + String source = "record Triangle (C top, C left, C right) { }"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withInterface() { + String source = "record Customer(String... data) implements Billable { }"; + builder.addSource( new StringReader(source) ); + } + + @Test + public void withAnnotatedParameters() { + String source = "record Rectangle(\n" + + " @GreaterThanZero double length,\n" + + " @GreaterThanZero double width) { }"; + builder.addSource( new StringReader(source) ); + } +}