Skip to content

Commit 2e52d75

Browse files
add record checker
1 parent ce9b8c3 commit 2e52d75

File tree

6 files changed

+152
-0
lines changed

6 files changed

+152
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.qulice.checkstyle;
2+
3+
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
4+
import com.puppycrawl.tools.checkstyle.api.DetailAST;
5+
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
6+
7+
/**
8+
* Checks for proper record declarations.
9+
* Validates that:
10+
* 1. Records are properly declared with components
11+
* 2. Record components are properly formatted
12+
* 3. Records do not extend other classes
13+
* 4. Records are final
14+
*/
15+
public class RecordValidationCheck extends AbstractCheck {
16+
/**
17+
* A key is pointing to the warning message text in "messages.properties"
18+
* file.
19+
*/
20+
public static final String MSG_KEY = "record.validation";
21+
22+
@Override
23+
public int[] getDefaultTokens() {
24+
return new int[] {
25+
TokenTypes.RECORD_DEF,
26+
TokenTypes.RECORD_COMPONENT_DEF
27+
};
28+
}
29+
30+
@Override
31+
public int[] getAcceptableTokens() {
32+
return getDefaultTokens();
33+
}
34+
35+
@Override
36+
public int[] getRequiredTokens() {
37+
return getDefaultTokens();
38+
}
39+
40+
@Override
41+
public void visitToken(DetailAST ast) {
42+
switch (ast.getType()) {
43+
case TokenTypes.RECORD_DEF:
44+
checkRecordDeclaration(ast);
45+
break;
46+
case TokenTypes.RECORD_COMPONENT_DEF:
47+
checkRecordComponent(ast);
48+
break;
49+
}
50+
}
51+
52+
private void checkRecordDeclaration(DetailAST ast) {
53+
// Check if record extends another class
54+
if (ast.findFirstToken(TokenTypes.EXTENDS_CLAUSE) != null) {
55+
log(ast.getLineNo(), ast.getColumnNo(), "Records cannot extend other classes");
56+
}
57+
58+
// Check if record is final
59+
DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
60+
if (modifiers != null && modifiers.findFirstToken(TokenTypes.FINAL) == null) {
61+
log(modifiers.getLineNo(), modifiers.getColumnNo(), "Records must be final");
62+
}
63+
64+
// Check if record has components
65+
DetailAST components = ast.findFirstToken(TokenTypes.RECORD_COMPONENTS);
66+
if (components != null && components.findFirstToken(TokenTypes.RECORD_COMPONENT_DEF) == null) {
67+
log(components.getLineNo(), components.getColumnNo(), "Records must declare at least one component");
68+
}
69+
70+
checkRecordInstanceFields(ast);
71+
}
72+
73+
private void checkRecordInstanceFields(DetailAST ast) {
74+
DetailAST objBlockAst = ast.findFirstToken(TokenTypes.OBJBLOCK);
75+
76+
// Traverse the children of the objBlockAst to find variable definitions
77+
for (DetailAST child = objBlockAst.getFirstChild(); child != null; child = child.getNextSibling()) {
78+
if (child.getType() == TokenTypes.VARIABLE_DEF) {
79+
DetailAST modifiersAst = child.findFirstToken(TokenTypes.MODIFIERS);
80+
DetailAST isStatic = modifiersAst.findFirstToken(TokenTypes.LITERAL_STATIC);
81+
82+
if (isStatic == null) {
83+
log(child.getLineNo(), child.getColumnNo(), "Records cannot have instance fields");
84+
}
85+
}
86+
}
87+
}
88+
89+
private void checkRecordComponent(DetailAST ast) {
90+
// Check if component has a type
91+
if (ast.findFirstToken(TokenTypes.TYPE) == null) {
92+
log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a type");
93+
}
94+
95+
// Check if component has a name
96+
if (ast.findFirstToken(TokenTypes.IDENT) == null) {
97+
log(ast.getLineNo(), ast.getColumnNo(), "Record component must have a name");
98+
}
99+
}
100+
}

qulice-checkstyle/src/test/java/com/qulice/checkstyle/ChecksTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ private void check(
151151
@SuppressWarnings("PMD.UnusedPrivateMethod")
152152
private static Stream<String> checks() {
153153
return Stream.of(
154+
"RecordValidationCheck",
154155
"MethodsOrderCheck",
155156
"MultilineJavadocTagsCheck",
156157
"StringLiteralsConcatenationCheck",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package com.qulice.modern;
6+
7+
/**
8+
* Invalid record example.
9+
*/
10+
// Records must be final
11+
public record InvalidRecord() { // Records must declare at least one component
12+
private String extraField; // Records cannot have instance fields
13+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package com.qulice.modern;
6+
7+
/**
8+
* Valid record example.
9+
*
10+
* @since 1.0
11+
*/
12+
public final record ValidRecord(String name, int age) {
13+
/**
14+
* Constructor.
15+
* @param name Name
16+
* @param age Age
17+
*/
18+
public ValidRecord {
19+
if (name == null || name.isEmpty()) {
20+
throw new IllegalArgumentException("Name cannot be empty");
21+
}
22+
if (age < 0) {
23+
throw new IllegalArgumentException("Age cannot be negative");
24+
}
25+
}
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE module PUBLIC
3+
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
4+
"https://checkstyle.org/dtds/configuration_1_3.dtd">
5+
<module name="Checker">
6+
<module name="TreeWalker">
7+
<module name="com.qulice.checkstyle.RecordValidationCheck"/>
8+
</module>
9+
</module>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
11:Records must be final
2+
11:Records must declare at least one component
3+
12:Records cannot have instance fields

0 commit comments

Comments
 (0)