Skip to content

Issue #18438: Fix false positive in EmptyLineSeparator check#18545

Merged
romani merged 1 commit into
checkstyle:masterfrom
Praveen7294:false
Apr 29, 2026
Merged

Issue #18438: Fix false positive in EmptyLineSeparator check#18545
romani merged 1 commit into
checkstyle:masterfrom
Praveen7294:false

Conversation

@Praveen7294

Copy link
Copy Markdown
Contributor

Fix #18438

@Praveen7294

Copy link
Copy Markdown
Contributor Author

Github, generate report for EmptyLineSeparator/all-examples-in-one

@github-actions

Copy link
Copy Markdown
Contributor

@Praveen7294

Copy link
Copy Markdown
Contributor Author

@Zopsss @romani i reviewed diff report everything looks fine.

Comment on lines +270 to +272
DetailAST reqAST = result;
if (reqAST.getNextSibling() != null) {
final Optional<DetailAST> postFixNode = getPostFixNode(reqAST.getNextSibling());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is reqAST?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reqAST is a temporary candidate node used during postfix handling.
It initially points to the current result, then allows the method to traverse into postfix-related AST structures (for example, method calls or increment expressions) without immediately overwriting result.

After the recursive traversal, reqAST is validated against the line constraint (reqAST.getLineNo() <= line). Only if it satisfies this condition is it promoted to result.

In short, reqAST acts as a safe checkpoint that holds a potential replacement for result while exploring postfix nodes, ensuring the final returned AST node still lies on or before the target line.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain it with AST?

Also the reqAST name doesn't make much sense, you need to rename it to something more meaningful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain it with AST?

        |       |--SEMI -> ; [19:63]
        |       |--EXPR -> EXPR [23:13]
        |       |   `--METHOD_CALL -> ( [23:13]
        |       |       |--DOT -> . [23:9]
        |       |       |   |--SINGLE_LINE_COMMENT -> // [22:8]
        |       |       |   |   `--COMMENT_CONTENT ->  violation 2 lines below 'There is more than 1 empty line after this line.'\r\n [22:10]
        |       |       |   |--IDENT -> t [23:8]
        |       |       |   `--IDENT -> foo [23:10]
        |       |       |--ELIST -> ELIST [23:14]
        |       |       |   `--EXPR -> EXPR [23:14]
        |       |       |       `--NUM_INT -> 10 [23:14]
        |       |       `--RPAREN -> ) [23:16]
        |       |--SEMI -> ; [23:17]

Looking at the AST, there are two empty lines between the first SEMI token (; [19:63]) and the next EXPR token (EXPR [23:13]). Therefore, according to the method’s intent, the SEMI at line 19 should be identified as the last AST element before the empty lines.

        if (result.getNextSibling() != null) {
            final Optional<DetailAST> postFixNode = getPostFixNode(result.getNextSibling());
                if (postFixNode.isPresent()) {
                // A post fix AST will always have a sibling METHOD CALL
                // METHOD CALL will at least have two children
                // The first child is DOT in case of POSTFIX which have at least 2 children
                // First child of DOT again puts us back to normal AST tree which will
                // recurse down below from here
                final DetailAST firstChildAfterPostFix = postFixNode.orElseThrow();
                result = getLastElementBeforeEmptyLines(firstChildAfterPostFix, line);
            }
        }

The postfix-handling logic inspects result.getNextSibling() and detects a method call expression. Because of this, it recursively descends into the method call AST and updates result to the SEMI at line 23 (; [23:17]). This overrides the correct result, even though that node appears after the empty lines, which leads to a false positive.

To avoid this, reqAST is introduced as a temporary candidate that initially refers to the current result. If a possible postfix or method call expression is found, traversal continues and reqAST is updated accordingly. Before promoting it to the final result, its line number is validated against the target line:

if (reqAST.getLineNo() <= line) {
    result = reqAST;
}

This ensures that postfix traversal does not incorrectly cross empty-line boundaries and that the returned AST node truly lies on or before the required line.

Also the reqAST name doesn't make much sense, you need to rename it to something more meaningful.

Is candidateAST an appropriate name? If not, could you please suggest a more meaningful one?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can code

DetailAST reqAST = result;
        if (reqAST.getNextSibling() != null) {
            final Optional<DetailAST> postFixNode = getPostFixNode(reqAST.getNextSibling());
            if (postFixNode.isPresent()) {
                // A post fix AST will always have a sibling METHOD CALL
                // METHOD CALL will at least have two children
                // The first child is DOT in case of POSTFIX which have at least 2 children
                // First child of DOT again puts us back to normal AST tree which will
                // recurse down below from here
                final DetailAST firstChildAfterPostFix = postFixNode.orElseThrow();
                reqAST = getLastElementBeforeEmptyLines(firstChildAfterPostFix, line);
            }
        }

        if (reqAST.getLineNo() <= line) {
            result = reqAST;
        }

be written like:

        result = positionToPotentialPostFixNode(result);
        

in this case comment can be moved to javadoc and logic will be a bit more structred.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romani Thanks for the help, it is now more structred.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look good now

@Praveen7294

Copy link
Copy Markdown
Contributor Author

@Zopsss please review, i explained as you asked.

@romani

romani commented Jan 30, 2026

Copy link
Copy Markdown
Member

@Praveen7294 , please se my comment above.
Please rebase , sorry that we lost tracking of this PR.

please consider #18438 (comment) also.

@romani

romani commented Jan 30, 2026

Copy link
Copy Markdown
Member

@Praveen7294 Praveen7294 force-pushed the false branch 2 times, most recently from de133b2 to 96b8b50 Compare January 30, 2026 22:05
@Praveen7294

Copy link
Copy Markdown
Contributor Author

please se my comment above.
Please rebase , sorry that we lost tracking of this PR.

Done.


please consider #18438 (comment) also.

This false positive occurs when we add below property in EmptyLineSeparatorCheck, caught during #18311 :

<property name="allowMultipleEmptyLinesInsideClassMembers" value="false"/>

This property is not added in google_checks.xml. So there is no need to add inputs for google style.

@Praveen7294

Copy link
Copy Markdown
Contributor Author

why we have this violation ?
https://checkstyle-diff-reports.s3.us-east-2.amazonaws.com/7ec4b2bad97c4c11059d33f0e846ca4ef5d6492a_2026053909/reports/diff/spring-framework/index.html#A1

is this different defect in same Check?

@romani The red one is for the base, and the green one is for the patch. Both are the same defect, but in the base code the violation references the wrong line, while in the patch code the violation references the correct line.

@Praveen7294 Praveen7294 requested review from Zopsss and romani January 31, 2026 15:06
@romani

romani commented Feb 3, 2026

Copy link
Copy Markdown
Member

Github, generate report for EmptyLineSeparator/all-examples-in-one

@github-actions

github-actions Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

@romani

romani commented Feb 3, 2026

Copy link
Copy Markdown
Member

Ok, I checking violations all good.
But

https://checkstyle-diff-reports.s3.us-east-2.amazonaws.com/96b8b50bbe62f142d0d4c1a228476862a61ea839_2026021830/reports/diff/spoon/index.html#A3 this looks like existing other problem.
Please check our issue tracker for it, if nothing, please create it.

@romani

romani commented Feb 3, 2026

Copy link
Copy Markdown
Member

I reviewed all from bottom until Orekit, please go though top to Orekit to spot unusual case, and let me know when you finished

@Praveen7294

Copy link
Copy Markdown
Contributor Author

I reviewed all from bottom until Orekit, please go though top to Orekit to spot unusual case, and let me know when you finished

I reviewed from top to openjdk25, there is no unusual violation.

@Praveen7294

Praveen7294 commented Feb 4, 2026

Copy link
Copy Markdown
Contributor Author

@romani

https://checkstyle-diff-reports.s3.us-east-2.amazonaws.com/96b8b50bbe62f142d0d4c1a228476862a61ea839_2026021830/reports/diff/spoon/index.html#A3 this looks like existing other problem.

i reviewed and found there is no handling case of anonymous class with method chaining.

Below example:

    public static void foo1() {
        Stack<Integer> stack = new Stack<>();
        stack.push(5);
 	    new EarlyTerminatingScanner<Void>() { // violation 'There is more than one empty line after this line.'



 	    }
 	    .setVisitCompilationUnitContent(true)
 	    .scan(element.getRoleInParent(), element);
    }

Please check our issue tracker for it, if nothing, please create it.

This is a case of chained method call and the issue is about method call, so it's better to solve in this PR.

Sorry for the delay. I was working on this problem to identify the main cause.

@romani

romani commented Feb 4, 2026

Copy link
Copy Markdown
Member

Please create issue, we can improve in separate issue/PR.

@Praveen7294

Copy link
Copy Markdown
Contributor Author

Please create issue, we can improve in separate issue/PR.

Done.

@romani

romani commented Feb 5, 2026

Copy link
Copy Markdown
Member

But this #18545 (comment) is just comment that Check consider as empty line
Check is comment aware

Not clear why it is new violation

@Praveen7294

Praveen7294 commented Feb 5, 2026

Copy link
Copy Markdown
Contributor Author

But thishttps://github.com/#18545 (comment) is just comment that Check consider as empty line
Check is comment aware

Yes, you are right. The check is comment-aware, but comments tokens are skipped(not consider as empty line) during AST traversal in the visitToken() method.

This one is to skip comment before current token

This one is to skip comment after current token

while (nextToken != null && TokenUtil.isCommentType(nextToken.getType())) {

but in one case comment tokens are not skipped when EmptyLineSeparatorCheck have to check empty line inside class member block, comment can occur inside class member block also. see below example for clarification:

Without comment:

import java.util.Stack;

public class Test {

    public static void foo1() {
        Stack<Integer> stack = new Stack<>();
        stack.push(5);
 	    new EarlyTerminatingScanner() {
             @Override
             public void visit(Object role, Object element) {
             
             }     // line 12
             



 	    }
            .scan();
    }
}

cli:

$ java -jar checkstyle-13.1.0-all.jar -c config_check.xml Test.java
Starting audit...
[ERROR] F:\GitHub\headhtmltagname\Test.java:12:14: '}' has more than 1 empty lines after. [EmptyLineSeparator]
[ERROR] F:\GitHub\headhtmltagname\Test.java:12:14: There is more than 1 empty line after this line. [EmptyLineSeparator]
Audit done.
Checkstyle ends with 2 errors.

With comment:

import java.util.Stack;

public class Test {

    public static void foo1() {
        Stack<Integer> stack = new Stack<>();
        stack.push(5);
 	    new EarlyTerminatingScanner() {
             @Override
             public void visit(Object role, Object element) {
             
             }        // line 12
             // test



 	    }
            .scan();
    }
}

cli:

$ java -jar checkstyle-13.1.0-all.jar -c config_check.xml Test.java
Starting audit...
[ERROR] F:\GitHub\headhtmltagname\Test.java:12:14: '}' has more than 1 empty lines after. [EmptyLineSeparator]
[ERROR] F:\GitHub\headhtmltagname\Test.java:13:16: There is more than 1 empty line after this line. [EmptyLineSeparator]
Audit done.
Checkstyle ends with 2 errors.

you can see after adding comment difference in second violation. This clearly shows comment is not skipped inside class member block. This explanation is only for clearing confusion around comment.

Not clear why it is new violation

This violation is due to anonymous class with chained method call. if you want i can explain with ast why this violation related to anonymous class with chained method call (not related to comment).

@romani

romani commented Feb 5, 2026

Copy link
Copy Markdown
Member

EmptyLineSeparatorCheck#example7 There is more than 1 empty line after this line. 146

public static ElementSourceFragment createSourceFragmentsFrom(CtElement element) {
144 		ElementSourceFragment rootFragment = new ElementSourceFragment(element, null);
145 		Deque<ElementSourceFragment> parents = new ArrayDeque<>();
146 		parents.push(rootFragment);
147 		/*
148 		 * scan all children of `element` and build tree of SourceFragments
149 		 * Note: we cannot skip implicit elements,
150 		 * because CtBlock can be implicit but contains non implicit elements, which has to be processed.
151 		 */
152 		new EarlyTerminatingScanner<Void>() {

I am human, and I don't understand why checkstyle thinks that after 146 lines there is more than 1 empty lines. It is not true for sure

@Praveen7294

Copy link
Copy Markdown
Contributor Author

I am human, and I don't understand why checkstyle thinks that after 146 lines there is more than 1 empty lines. It is not true for sure

see line 160. After this line, there are more than one empty lines. Due to this, the check tries to find the last token before the empty line. However, because of the chained method call, it is unable to determine the correct last token before the empty line.

@Praveen7294

Copy link
Copy Markdown
Contributor Author

@romani can we move forward this PR, remaining false positive cases will resolve in #18858 issue. if you are ok with my clarification.

@Praveen7294 Praveen7294 force-pushed the false branch 2 times, most recently from a061750 to 15d2caf Compare March 19, 2026 11:56
@Praveen7294 Praveen7294 force-pushed the false branch 2 times, most recently from bdc820f to 9e6fad1 Compare March 25, 2026 14:43
@romani

romani commented Mar 25, 2026

Copy link
Copy Markdown
Member

Github, generate report for EmptyLineSeparator/all-examples-in-one

@github-actions

Copy link
Copy Markdown
Contributor

@Praveen7294 Praveen7294 force-pushed the false branch 2 times, most recently from 3f09e03 to 2ec001b Compare March 31, 2026 15:18
@Praveen7294

Copy link
Copy Markdown
Contributor Author

@romani ping

@romani

romani commented Apr 11, 2026

Copy link
Copy Markdown
Member

Github, generate report for EmptyLineSeparator/all-examples-in-one

@github-actions

Copy link
Copy Markdown
Contributor

@romani romani left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks a lot for this update.
All good.

@Zopsss Zopsss left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Praveen7294

Copy link
Copy Markdown
Contributor Author

@romani and @Zopsss, since you both approved my changes, is it okay to merge? I have rebased my PR branch.

@romani romani merged commit b884b9a into checkstyle:master Apr 29, 2026
123 checks passed
@romani

romani commented Apr 29, 2026

Copy link
Copy Markdown
Member

Thanks a lot , sorry that we lost this PR is tracking

@Praveen7294 Praveen7294 deleted the false branch May 9, 2026 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False positive: EmptyLineSeparator reports violation on method call

3 participants