diff --git a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java index 776525295..75861dd39 100644 --- a/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java +++ b/maddi-java-openjdk/src/main/java/io/codelaser/maddi/java/openjdk/SourceCodeScan.java @@ -522,6 +522,29 @@ private void scanMethodDeclaration(Node md) { } } } + addConstructorBodyTrailingComments(md); + } + + /* + A constructor's body is not a CodeBlock in the parser's tree: its '{', its statements and its '}' are children of the + ConstructorDeclaration itself (Java.ccc, ConstructorDeclaration), where a method's body is one CodeBlock child. The + comments in front of a body's closing '}' -- the body's trailing comments -- are recorded for CodeBlocks only + (scanCodeBlock), so in a constructor they were recorded nowhere: `C() { // inside }` parsed and printed as `C(){}`, + and `C() { int i = 0; // after }` as `C(){int i=0;}` (TestCommentInEmptyConstructor). Recorded here under the body's + source, from '{' to '}', which is where ScanCompilationUnit.parseBlock looks them up for the javac block. + */ + private void addConstructorBodyTrailingComments(Node md) { + if (!(md instanceof ConstructorDeclaration)) return; + Node open = null; + for (Node child : md.children()) { + if (child instanceof Delimiter d && d.getType() == Token.TokenType.LBRACE) { + open = child; + break; + } + } + Node close = md.getLastChild(); + if (open == null || !(close instanceof Delimiter d && d.getType() == Token.TokenType.RBRACE)) return; + addTrailingComments(source(open, close), close); } // collects the comma delimiters that are direct children of listNode, in the same shape as ARGUMENT_COMMAS diff --git a/maddi-java-openjdk/src/test/java/io/codelaser/maddi/java/openjdk/statement/TestCommentInEmptyConstructor.java b/maddi-java-openjdk/src/test/java/io/codelaser/maddi/java/openjdk/statement/TestCommentInEmptyConstructor.java new file mode 100644 index 000000000..df8920e52 --- /dev/null +++ b/maddi-java-openjdk/src/test/java/io/codelaser/maddi/java/openjdk/statement/TestCommentInEmptyConstructor.java @@ -0,0 +1,115 @@ +/* + * maddi: a modification analyzer for duplication detection and immutability. + * Copyright 2020-2025, Bart Naudts, https://github.com/CodeLaser/maddi + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. You should have received a copy of the GNU Lesser General Public + * License along with this program. If not, see . + */ + +package io.codelaser.maddi.java.openjdk.statement; + +import io.codelaser.maddi.cst.api.info.MethodInfo; +import io.codelaser.maddi.cst.api.info.TypeInfo; +import io.codelaser.maddi.java.openjdk.CommonTest; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * A comment that is the only thing in a body, or that follows the body's last statement, must survive parsing: + * printing the member gives it back. + *

+ * ⛔ In a CONSTRUCTOR it does not. {@code C() { // inside }} parses to a constructor with no comment anywhere -- not + * on the constructor, not on its body (leading or trailing), not on the implicit {@code super()} -- and prints as + * {@code C(){}}. The same body in a method keeps its comment, which is the control below. A tool that rewrites the + * constructor (a dependency-injection migration replacing it by an injected one) therefore cannot see the comment + * and deletes it with the constructor. + */ +public class TestCommentInEmptyConstructor extends CommonTest { + + @Language("java") + private static final String CONSTRUCTOR = """ + package a.b; + class C { + C() { + // inside + } + } + """; + + @Language("java") + private static final String METHOD = """ + package a.b; + class M { + void m() { + // inside + } + } + """; + + @Language("java") + private static final String CONSTRUCTOR_COMMENT_LAST = """ + package a.b; + class D { + D() { + int i = 0; + // after + } + } + """; + + @Language("java") + private static final String METHOD_COMMENT_LAST = """ + package a.b; + class N { + void n() { + int i = 0; + // after + } + } + """; + + @DisplayName("⛔ a comment alone in a constructor's body is kept") + @Test + public void constructor() { + TypeInfo typeInfo = scan("a.b.C", CONSTRUCTOR); + MethodInfo constructor = typeInfo.constructors().getFirst(); + assertEquals("C(){// inside\n}", constructor.print(runtime.qualificationQualifyFromPrimaryType()).toString(), + "first: the constructor as it must print; second: as maddi prints it"); + } + + @DisplayName("control: a comment alone in a method's body is kept") + @Test + public void method() { + TypeInfo typeInfo = scan("a.b.M", METHOD); + MethodInfo method = typeInfo.findUniqueMethod("m", 0); + assertEquals("void m(){// inside\n}", method.print(runtime.qualificationQualifyFromPrimaryType()).toString(), + "first: the method as it must print; second: as maddi prints it"); + } + + @DisplayName("⛔ a comment after the last statement of a constructor's body is kept") + @Test + public void constructorCommentAfterLastStatement() { + TypeInfo typeInfo = scan("a.b.D", CONSTRUCTOR_COMMENT_LAST); + MethodInfo constructor = typeInfo.constructors().getFirst(); + assertEquals("D(){int i=0;// after\n}", constructor.print(runtime.qualificationQualifyFromPrimaryType()).toString(), + "first: the constructor as it must print; second: as maddi prints it"); + } + + @DisplayName("control: a comment after the last statement of a method's body is kept") + @Test + public void methodCommentAfterLastStatement() { + TypeInfo typeInfo = scan("a.b.N", METHOD_COMMENT_LAST); + MethodInfo method = typeInfo.findUniqueMethod("n", 0); + assertEquals("void n(){int i=0;// after\n}", method.print(runtime.qualificationQualifyFromPrimaryType()).toString(), + "first: the method as it must print; second: as maddi prints it"); + } +}