Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/mvn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
os: [windows-2022, ubuntu-24.04, macos-15]
java: [11, 21]
java: [11, 17, 21]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
* SPDX-License-Identifier: MIT
*/
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
* @since 0.24
*/
public final 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 this.getDefaultTokens();
}

@Override
public int[] getRequiredTokens() {
return this.getDefaultTokens();
}

@Override
public void visitToken(final DetailAST ast) {
switch (ast.getType()) {
case TokenTypes.RECORD_DEF:
this.checkRecordDeclaration(ast);
break;
case TokenTypes.RECORD_COMPONENT_DEF:
this.checkRecordComponent(ast);
break;
default:
}
}

/**
* 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) {
this.log(ast.getLineNo(), ast.getColumnNo(), "Records cannot extend other classes");
}
final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
if (modifiers != null && modifiers.findFirstToken(TokenTypes.FINAL) == null) {
this.log(modifiers.getLineNo(), modifiers.getColumnNo(), "Records must be final");
}
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"
);
}
this.checkRecordInstanceFields(ast);
}

/**
* 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) {
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();
}
}

/**
* 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) {
this.log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a type");
}
if (ast.findFirstToken(TokenTypes.IDENT) == null) {
this.log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a name");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ private void check(
@SuppressWarnings("PMD.UnusedPrivateMethod")
private static Stream<String> checks() {
return Stream.of(
"RecordValidationCheck",
"MethodsOrderCheck",
"MultilineJavadocTagsCheck",
"StringLiteralsConcatenationCheck",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
* SPDX-License-Identifier: MIT
-->
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="TreeWalker">
<module name="com.qulice.checkstyle.RecordValidationCheck"/>
</module>
</module>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
11:Records must be final
11:Records must declare at least one component
12:Records cannot have instance fields
Loading