diff --git a/README.md b/README.md index fb5f6f5fa..fa0846aac 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -logo +# Qulice +![logo](https://www.qulice.com/logo.svg) [![EO principles respected here](https://www.elegantobjects.org/badge.svg)](https://www.elegantobjects.org) [![DevOps By Rultor.com](https://www.rultor.com/b/yegor256/qulice)](https://www.rultor.com/p/yegor256/qulice) [![We recommend IntelliJ IDEA](https://www.elegantobjects.org/intellij-idea.svg)](https://www.jetbrains.com/idea/) @@ -72,7 +73,7 @@ they don't violate our quality standards. To avoid frustration, before sending us your pull request please run full Maven build: ```bash -$ mvn clean install -Pqulice +mvn clean install -Pqulice ``` Keep in mind that JDK 11+ and Maven 3.8+ are the lowest versions you may use. diff --git a/qulice-pmd/src/main/java/com/qulice/pmd/rules/PrimaryConstructorLastRule.java b/qulice-pmd/src/main/java/com/qulice/pmd/rules/PrimaryConstructorLastRule.java new file mode 100644 index 000000000..4f1b19e4a --- /dev/null +++ b/qulice-pmd/src/main/java/com/qulice/pmd/rules/PrimaryConstructorLastRule.java @@ -0,0 +1,108 @@ +/** + * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko + * SPDX-License-Identifier: MIT + * Check that primary constructor is placed at the end of constructors list. + * + * This rule checks that the constructor with the most parameters (primary constructor) + * is placed after all other constructors in the class. + */ +package com.qulice.pmd.rules; + +import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration; +import net.sourceforge.pmd.lang.java.ast.ASTFormalParameters; +import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; + +import java.util.ArrayList; +import java.util.List; +import java.util.Comparator; + +/** + * Rule that checks primary constructor is placed at the end. + * Primary constructor is the one with the most parameters. + * + * @since 0.18 + */ +public final class PrimaryConstructorLastRule extends AbstractJavaRule { + + /** + * Error message for the rule violation. + */ + private static final String MESSAGE = + "Primary constructor (with most parameters) should be placed at the end of constructors list"; + + @Override + public Object visit(ASTClassOrInterfaceDeclaration node, Object data) { + // Only check classes, not interfaces + if (!node.isInterface()) { + checkConstructorOrder(node, data); + } + return super.visit(node, data); + } + + /** + * Check the order of constructors in the class. + * + * @param classNode Class declaration node + * @param data Rule context data + */ + private void checkConstructorOrder(ASTClassOrInterfaceDeclaration classNode, Object data) { + List constructors = + classNode.findDescendantsOfType(ASTConstructorDeclaration.class); + + // Filter only direct children constructors (not nested class constructors) + List directConstructors = new ArrayList<>(); + for (ASTConstructorDeclaration constructor : constructors) { + if (constructor.getFirstParentOfType(ASTClassOrInterfaceDeclaration.class) == classNode) { + directConstructors.add(constructor); + } + } + + if (directConstructors.size() <= 1) { + // No need to check order if there's only one or no constructors + return; + } + + // Find the primary constructor (with most parameters) + ASTConstructorDeclaration primaryConstructor = findPrimaryConstructor(directConstructors); + if (primaryConstructor == null) { + return; + } + + // Check if primary constructor is the last one + ASTConstructorDeclaration lastConstructor = directConstructors.get(directConstructors.size() - 1); + if (primaryConstructor != lastConstructor) { + asCtx(data).addViolation(primaryConstructor, MESSAGE); + } + } + + /** + * Find primary constructor (the one with most parameters). + * If there are multiple constructors with the same max parameter count, + * consider the first one as primary. + * + * @param constructors List of constructors + * @return Primary constructor or null if no constructors + */ + private ASTConstructorDeclaration findPrimaryConstructor( + List constructors) { + if (constructors.isEmpty()) { + return null; + } + + return constructors.stream() + .max(Comparator.comparingInt(this::getParameterCount)) + .orElse(null); + } + + /** + * Get parameter count for a constructor. + * + * @param constructor Constructor declaration + * @return Number of parameters + */ + private int getParameterCount(ASTConstructorDeclaration constructor) { + ASTFormalParameters params = constructor.getFirstDescendantOfType(ASTFormalParameters.class); + return params != null ? params.size() : 0; + } +} \ No newline at end of file diff --git a/qulice-pmd/src/main/resources/com/qulice/pmd/ruleset.xml b/qulice-pmd/src/main/resources/com/qulice/pmd/ruleset.xml index 66e0b8eef..1cdad9abc 100644 --- a/qulice-pmd/src/main/resources/com/qulice/pmd/ruleset.xml +++ b/qulice-pmd/src/main/resources/com/qulice/pmd/ruleset.xml @@ -61,6 +61,13 @@ + + + This rule checks that the constructor with the most parameters (primary constructor) + is placed after all other constructors in the class. This convention improves + code readability by placing the most complete constructor at the end. + + Instead of using plain JUnit assertions like org.junit.Assert.assert* @@ -102,7 +109,14 @@ 1] - [count(ClassOrInterfaceBodyDeclaration/ConstructorDeclaration[BlockStatement])>1] + [count( + ClassOrInterfaceBodyDeclaration/ConstructorDeclaration[ + BlockStatement[not( + Statement/StatementExpression/PrimaryExpression/PrimaryPrefix[@ThisModifier="true"] + or Statement/StatementExpression/PrimaryExpression/PrimaryPrefix[@SuperModifier="true"] + )] + ] + )>1] ]]>