From 2e52d755b09ef376d9fe090255933bfd9d39e3df Mon Sep 17 00:00:00 2001 From: AruzhanShinbayeva Date: Mon, 12 May 2025 01:03:39 +0300 Subject: [PATCH 1/6] add record checker --- .../checkstyle/RecordValidationCheck.java | 100 ++++++++++++++++++ .../com/qulice/checkstyle/ChecksTest.java | 1 + .../RecordValidationCheck/Invalid.java | 13 +++ .../RecordValidationCheck/Valid.java | 26 +++++ .../RecordValidationCheck/config.xml | 9 ++ .../RecordValidationCheck/violations.txt | 3 + 6 files changed, 152 insertions(+) create mode 100644 qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java create mode 100644 qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Invalid.java create mode 100644 qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Valid.java create mode 100644 qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml create mode 100644 qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/violations.txt diff --git a/qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java b/qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java new file mode 100644 index 000000000..6d2d0b8ab --- /dev/null +++ b/qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java @@ -0,0 +1,100 @@ +package com.qulice.checkstyle; + +import com.puppycrawl.tools.checkstyle.api.AbstractCheck; +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.TokenTypes; + +/** + * Checks for proper record declarations. + * Validates that: + * 1. Records are properly declared with components + * 2. Record components are properly formatted + * 3. Records do not extend other classes + * 4. Records are final + */ +public class RecordValidationCheck extends AbstractCheck { + /** + * A key is pointing to the warning message text in "messages.properties" + * file. + */ + public static final String MSG_KEY = "record.validation"; + + @Override + public int[] getDefaultTokens() { + return new int[] { + TokenTypes.RECORD_DEF, + TokenTypes.RECORD_COMPONENT_DEF + }; + } + + @Override + public int[] getAcceptableTokens() { + return getDefaultTokens(); + } + + @Override + public int[] getRequiredTokens() { + return getDefaultTokens(); + } + + @Override + public void visitToken(DetailAST ast) { + switch (ast.getType()) { + case TokenTypes.RECORD_DEF: + checkRecordDeclaration(ast); + break; + case TokenTypes.RECORD_COMPONENT_DEF: + checkRecordComponent(ast); + break; + } + } + + private void checkRecordDeclaration(DetailAST ast) { + // Check if record extends another class + if (ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE) != null) { + log(ast.getLineNo(), ast.getColumnNo(), "Records cannot extend other classes"); + } + + // Check if record is final + DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); + if (modifiers != null && modifiers.findFirstToken(TokenTypes.FINAL) == null) { + log(modifiers.getLineNo(), modifiers.getColumnNo(), "Records must be final"); + } + + // Check if record has components + DetailAST components = ast.findFirstToken(TokenTypes.RECORD_COMPONENTS); + if (components != null && components.findFirstToken(TokenTypes.RECORD_COMPONENT_DEF) == null) { + log(components.getLineNo(), components.getColumnNo(), "Records must declare at least one component"); + } + + checkRecordInstanceFields(ast); + } + + private void checkRecordInstanceFields(DetailAST ast) { + DetailAST objBlockAst = ast.findFirstToken(TokenTypes.OBJBLOCK); + + // Traverse the children of the objBlockAst to find variable definitions + for (DetailAST child = objBlockAst.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getType() == TokenTypes.VARIABLE_DEF) { + DetailAST modifiersAst = child.findFirstToken(TokenTypes.MODIFIERS); + DetailAST isStatic = modifiersAst.findFirstToken(TokenTypes.LITERAL_STATIC); + + if (isStatic == null) { + log(child.getLineNo(), child.getColumnNo(), "Records cannot have instance fields"); + } + } + } + } + + private void checkRecordComponent(DetailAST ast) { + // Check if component has a type + if (ast.findFirstToken(TokenTypes.TYPE) == null) { + log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a type"); + } + + // Check if component has a name + if (ast.findFirstToken(TokenTypes.IDENT) == null) { + log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a name"); + } + } +} \ No newline at end of file diff --git a/qulice-checkstyle/src/test/java/com/qulice/checkstyle/ChecksTest.java b/qulice-checkstyle/src/test/java/com/qulice/checkstyle/ChecksTest.java index 150c01dcc..f97c92440 100644 --- a/qulice-checkstyle/src/test/java/com/qulice/checkstyle/ChecksTest.java +++ b/qulice-checkstyle/src/test/java/com/qulice/checkstyle/ChecksTest.java @@ -151,6 +151,7 @@ private void check( @SuppressWarnings("PMD.UnusedPrivateMethod") private static Stream checks() { return Stream.of( + "RecordValidationCheck", "MethodsOrderCheck", "MultilineJavadocTagsCheck", "StringLiteralsConcatenationCheck", diff --git a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Invalid.java b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Invalid.java new file mode 100644 index 000000000..6d6a6b449 --- /dev/null +++ b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Invalid.java @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko + * SPDX-License-Identifier: MIT + */ +package com.qulice.modern; + +/** + * Invalid record example. + */ +// Records must be final +public record InvalidRecord() { // Records must declare at least one component + private String extraField; // Records cannot have instance fields +} \ No newline at end of file diff --git a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Valid.java b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Valid.java new file mode 100644 index 000000000..6c02ae2ea --- /dev/null +++ b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/Valid.java @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko + * SPDX-License-Identifier: MIT + */ +package com.qulice.modern; + +/** + * Valid record example. + * + * @since 1.0 + */ +public final record ValidRecord(String name, int age) { + /** + * Constructor. + * @param name Name + * @param age Age + */ + public ValidRecord { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("Name cannot be empty"); + } + if (age < 0) { + throw new IllegalArgumentException("Age cannot be negative"); + } + } +} \ No newline at end of file diff --git a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml new file mode 100644 index 000000000..a9ad3c4c4 --- /dev/null +++ b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/violations.txt b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/violations.txt new file mode 100644 index 000000000..16ea61f46 --- /dev/null +++ b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/violations.txt @@ -0,0 +1,3 @@ +11:Records must be final +11:Records must declare at least one component +12:Records cannot have instance fields \ No newline at end of file From 7628fa18da5dfa397fb74aaf059817e0aae4b39d Mon Sep 17 00:00:00 2001 From: AruzhanShinbayeva Date: Mon, 12 May 2025 01:07:33 +0300 Subject: [PATCH 2/6] try java 17 --- .github/workflows/mvn.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mvn.yml b/.github/workflows/mvn.yml index c224b0e89..431585a27 100644 --- a/.github/workflows/mvn.yml +++ b/.github/workflows/mvn.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [windows-2022, ubuntu-24.04, macos-15] - java: [11, 21] + java: [17, 21] steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 From e5b2e2cd75b8cba1db69e616c6b70cc5528276bb Mon Sep 17 00:00:00 2001 From: AruzhanShinbayeva Date: Mon, 12 May 2025 01:09:12 +0300 Subject: [PATCH 3/6] look error --- .github/workflows/mvn.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mvn.yml b/.github/workflows/mvn.yml index 431585a27..132ae9c6e 100644 --- a/.github/workflows/mvn.yml +++ b/.github/workflows/mvn.yml @@ -30,4 +30,4 @@ jobs: key: ${{ runner.os }}-jdk-${{ matrix.java }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-jdk-${{ matrix.java }}-maven- - - run: mvn -B install -Pqulice + - run: mvn -B install -Pqulice -e From 5fa60e087cd1ace5d92c990223ffea5156d9b8c1 Mon Sep 17 00:00:00 2001 From: AruzhanShinbayeva Date: Mon, 12 May 2025 01:39:30 +0300 Subject: [PATCH 4/6] fix issues --- .github/workflows/mvn.yml | 2 +- .../checkstyle/RecordValidationCheck.java | 98 +++++++++++-------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/.github/workflows/mvn.yml b/.github/workflows/mvn.yml index 132ae9c6e..431585a27 100644 --- a/.github/workflows/mvn.yml +++ b/.github/workflows/mvn.yml @@ -30,4 +30,4 @@ jobs: key: ${{ runner.os }}-jdk-${{ matrix.java }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-jdk-${{ matrix.java }}-maven- - - run: mvn -B install -Pqulice -e + - run: mvn -B install -Pqulice diff --git a/qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java b/qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java index 6d2d0b8ab..31f797fa5 100644 --- a/qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java +++ b/qulice-checkstyle/src/main/java/com/qulice/checkstyle/RecordValidationCheck.java @@ -1,3 +1,7 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko + * SPDX-License-Identifier: MIT + */ package com.qulice.checkstyle; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; @@ -11,8 +15,9 @@ * 2. Record components are properly formatted * 3. Records do not extend other classes * 4. Records are final + * @since 0.24 */ -public class RecordValidationCheck extends AbstractCheck { +public final class RecordValidationCheck extends AbstractCheck { /** * A key is pointing to the warning message text in "messages.properties" * file. @@ -22,79 +27,94 @@ public class RecordValidationCheck extends AbstractCheck { @Override public int[] getDefaultTokens() { return new int[] { - TokenTypes.RECORD_DEF, - TokenTypes.RECORD_COMPONENT_DEF + TokenTypes.RECORD_DEF, + TokenTypes.RECORD_COMPONENT_DEF, }; } @Override public int[] getAcceptableTokens() { - return getDefaultTokens(); + return this.getDefaultTokens(); } @Override public int[] getRequiredTokens() { - return getDefaultTokens(); + return this.getDefaultTokens(); } @Override - public void visitToken(DetailAST ast) { + public void visitToken(final DetailAST ast) { switch (ast.getType()) { case TokenTypes.RECORD_DEF: - checkRecordDeclaration(ast); + this.checkRecordDeclaration(ast); break; case TokenTypes.RECORD_COMPONENT_DEF: - checkRecordComponent(ast); + this.checkRecordComponent(ast); break; + default: } } - private void checkRecordDeclaration(DetailAST ast) { - // Check if record extends another class + /** + * Check if record extends another class. + * Check if record is final. + * Check if record has components. + * @param ast EXPR RECORD_DEF node that needs to be checked + */ + private void checkRecordDeclaration(final DetailAST ast) { if (ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE) != null) { - log(ast.getLineNo(), ast.getColumnNo(), "Records cannot extend other classes"); + this.log(ast.getLineNo(), ast.getColumnNo(), "Records cannot extend other classes"); } - - // Check if record is final - DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); + final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); if (modifiers != null && modifiers.findFirstToken(TokenTypes.FINAL) == null) { - log(modifiers.getLineNo(), modifiers.getColumnNo(), "Records must be final"); + this.log(modifiers.getLineNo(), modifiers.getColumnNo(), "Records must be final"); } - - // Check if record has components - DetailAST components = ast.findFirstToken(TokenTypes.RECORD_COMPONENTS); - if (components != null && components.findFirstToken(TokenTypes.RECORD_COMPONENT_DEF) == null) { - log(components.getLineNo(), components.getColumnNo(), "Records must declare at least one component"); + final DetailAST components = ast.findFirstToken(TokenTypes.RECORD_COMPONENTS); + if (components != null + && components.findFirstToken(TokenTypes.RECORD_COMPONENT_DEF) == null) { + this.log( + components.getLineNo(), + components.getColumnNo(), + "Records must declare at least one component" + ); } - - checkRecordInstanceFields(ast); + this.checkRecordInstanceFields(ast); } - private void checkRecordInstanceFields(DetailAST ast) { - DetailAST objBlockAst = ast.findFirstToken(TokenTypes.OBJBLOCK); - - // Traverse the children of the objBlockAst to find variable definitions - for (DetailAST child = objBlockAst.getFirstChild(); child != null; child = child.getNextSibling()) { + /** + * Check record does not contain instance field. + * @param ast EXPR RECORD_DEF node that needs to be checked + */ + private void checkRecordInstanceFields(final DetailAST ast) { + final DetailAST block = ast.findFirstToken(TokenTypes.OBJBLOCK); + DetailAST child = block.getFirstChild(); + while (child != null) { if (child.getType() == TokenTypes.VARIABLE_DEF) { - DetailAST modifiersAst = child.findFirstToken(TokenTypes.MODIFIERS); - DetailAST isStatic = modifiersAst.findFirstToken(TokenTypes.LITERAL_STATIC); - - if (isStatic == null) { - log(child.getLineNo(), child.getColumnNo(), "Records cannot have instance fields"); + final DetailAST modifiers = child.findFirstToken(TokenTypes.MODIFIERS); + final DetailAST statics = modifiers.findFirstToken(TokenTypes.LITERAL_STATIC); + if (statics == null) { + this.log( + child.getLineNo(), + child.getColumnNo(), + "Records cannot have instance fields" + ); } } + child = child.getNextSibling(); } } - private void checkRecordComponent(DetailAST ast) { - // Check if component has a type + /** + * Check if component has a type. + * Check if component has a name. + * @param ast EXPR RECORD_COMPONENT_DEF node that needs to be checked + */ + private void checkRecordComponent(final DetailAST ast) { if (ast.findFirstToken(TokenTypes.TYPE) == null) { - log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a type"); + this.log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a type"); } - - // Check if component has a name if (ast.findFirstToken(TokenTypes.IDENT) == null) { - log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a name"); + this.log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a name"); } } -} \ No newline at end of file +} From ebc78a94e39ca90101b2784457850043ec962bc9 Mon Sep 17 00:00:00 2001 From: AruzhanShinbayeva Date: Mon, 12 May 2025 01:41:38 +0300 Subject: [PATCH 5/6] fix copyright --- .../checkstyle/ChecksTest/RecordValidationCheck/config.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml index a9ad3c4c4..2915c0fd8 100644 --- a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml +++ b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml @@ -1,4 +1,8 @@ + From 8fb17ab6b927d60ede30d1e84388cc750721a62e Mon Sep 17 00:00:00 2001 From: AruzhanShinbayeva Date: Mon, 12 May 2025 01:49:46 +0300 Subject: [PATCH 6/6] try run java 11 --- .github/workflows/mvn.yml | 2 +- .../checkstyle/ChecksTest/RecordValidationCheck/config.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mvn.yml b/.github/workflows/mvn.yml index 431585a27..cacbeb123 100644 --- a/.github/workflows/mvn.yml +++ b/.github/workflows/mvn.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [windows-2022, ubuntu-24.04, macos-15] - java: [17, 21] + java: [11, 17, 21] steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 diff --git a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml index 2915c0fd8..d0cbe89c1 100644 --- a/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml +++ b/qulice-checkstyle/src/test/resources/com/qulice/checkstyle/ChecksTest/RecordValidationCheck/config.xml @@ -10,4 +10,4 @@ - \ No newline at end of file + \ No newline at end of file