From 24ff96afe41b62275fe8635e477ecc04bff93123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Walln=C3=B6fer?= Date: Tue, 8 Apr 2025 18:45:53 +0000 Subject: [PATCH] 8352389: Remove incidental whitespace in pre/code content Reviewed-by: liach --- .../tools/javac/parser/DocCommentParser.java | 129 ++++++++++++++ .../formats/html/resources/stylesheet.css | 2 +- .../TestLiteralCodeInPre.java | 11 +- .../doclet/testPreCode/TestPreCode.java | 165 ++++++++++++++++++ .../tools/javac/doctree/CodeTest.java | 4 +- .../tools/javac/doctree/DocCommentTester.java | 4 +- .../tools/javac/doctree/InPreTest.java | 46 ++++- 7 files changed, 349 insertions(+), 12 deletions(-) create mode 100644 test/langtools/jdk/javadoc/doclet/testPreCode/TestPreCode.java diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/DocCommentParser.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/DocCommentParser.java index 017c39b3c54..cf545a8f520 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/DocCommentParser.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/DocCommentParser.java @@ -34,8 +34,12 @@ import java.util.regex.Pattern; import com.sun.source.doctree.AttributeTree.ValueKind; import com.sun.source.doctree.DocTree; import com.sun.source.doctree.ErroneousTree; +import com.sun.source.doctree.LiteralTree; +import com.sun.source.doctree.StartElementTree; +import com.sun.source.doctree.TextTree; import com.sun.source.doctree.UnknownBlockTagTree; import com.sun.source.doctree.UnknownInlineTagTree; +import com.sun.source.util.SimpleDocTreeVisitor; import com.sun.tools.javac.parser.Tokens.Comment; import com.sun.tools.javac.tree.DCTree; import com.sun.tools.javac.tree.DCTree.DCAttribute; @@ -126,6 +130,9 @@ public class DocCommentParser { private int lastNonWhite = -1; private boolean newline = true; + /** Used for whitespace normalization in pre/code/literal tags. */ + private boolean inPre = false; + private final Map tagParsers; /** @@ -281,6 +288,7 @@ public class DocCommentParser { */ protected List content(Phase phase) { ListBuffer trees = new ListBuffer<>(); + ListBuffer mainTrees = null; textStart = -1; int depth = 1; // only used when phase is INLINE @@ -349,6 +357,18 @@ public class DocCommentParser { addPendingText(trees, bp - 1); trees.add(html()); + // Create new list for
 content which is merged into main list when element is closed.
+                            if (inPre) {
+                                if (mainTrees == null) {
+                                    mainTrees = trees;
+                                    trees = new ListBuffer<>();
+                                }
+                            } else if (mainTrees != null) {
+                                mainTrees.addAll(normalizePreContent(trees));
+                                trees = mainTrees;
+                                mainTrees = null;
+                            }
+
                             if (phase == Phase.PREAMBLE || phase == Phase.POSTAMBLE) {
                                 break; // Ignore newlines after html tags, in the meta content
                             }
@@ -476,6 +496,13 @@ public class DocCommentParser {
         if (lastNonWhite != -1)
             addPendingText(trees, lastNonWhite);
 
+        // Happens with unclosed 
 element. Add content without normalizing.
+        if (mainTrees != null) {
+            mainTrees.addAll(trees);
+            trees = mainTrees;
+            mainTrees = null;
+        }
+
         return (phase == Phase.INLINE)
                 ? List.of(erroneous("dc.unterminated.inline.tag", pos))
                 : trees.toList();
@@ -1054,6 +1081,9 @@ public class DocCommentParser {
                 }
                 if (ch == '>') {
                     nextChar();
+                    if ("pre".equalsIgnoreCase(name.toString())) {
+                        inPre = true;
+                    }
                     return m.at(p).newStartElementTree(name, attrs, selfClosing).setEndPos(bp);
                 }
             }
@@ -1064,6 +1094,9 @@ public class DocCommentParser {
                 skipWhitespace();
                 if (ch == '>') {
                     nextChar();
+                    if ("pre".equalsIgnoreCase(name.toString())) {
+                        inPre = false;
+                    }
                     return m.at(p).newEndElementTree(name).setEndPos(bp);
                 }
             }
@@ -1186,6 +1219,102 @@ public class DocCommentParser {
         return attrs.toList();
     }
 
+    /*
+     * Removes a newline character following a , {@code or {@literal tag at the
+     * beginning of 
 element content, as well as any space/tabs between the pre
+     * and code tags. The operation is only performed on traditional doc comments.
+     * If conditions are not met the list is returned unchanged.
+     */
+    ListBuffer normalizePreContent(ListBuffer trees) {
+        // Do nothing if comment is not eligible for whitespace normalization.
+        if (textKind == DocTree.Kind.MARKDOWN || isHtmlFile) {
+            return trees;
+        }
+
+        enum State {
+            BEFORE_CODE, // at beginning of 
 content, or after leading horizontal whitespace
+            AFTER_CODE,  // after  start tag (not used for {@code} tag)
+            SUCCEEDED,   // normalization succeeded, add remaining trees
+            FAILED;      // normalization failed, return original trees
+        }
+
+        class Context {
+            State state = State.BEFORE_CODE;
+
+            // Called when an unexpected tree is encountered. Set state to
+            // FAILED unless normalization already terminated successfully.
+            void unexpectedTree() {
+                if (state != State.SUCCEEDED) {
+                    state = State.FAILED;
+                }
+            }
+        }
+
+        var visitor = new SimpleDocTreeVisitor() {
+            @Override
+            public DCTree visitText(TextTree text, Context cx) {
+                if (cx.state == State.BEFORE_CODE && text.getBody().matches("[ \t]+")) {
+                    // 
  ...
+                    return null;
+                } else if (cx.state == State.AFTER_CODE && text.getBody().startsWith("\n")) {
+                    // 
\n...
+                    cx.state = State.SUCCEEDED;
+                    return m.at(((DCText) text).pos + 1).newTextTree(text.getBody().substring(1));
+                }
+                cx.unexpectedTree();
+                return (DCTree) text;
+            }
+
+            @Override
+            public DCTree visitLiteral(LiteralTree literal, Context cx) {
+                if (cx.state == State.BEFORE_CODE && literal.getBody().getBody().startsWith("\n")) {
+                    // 
{@code\n...
+                    cx.state = State.SUCCEEDED;
+                    DCText oldBody = (DCText) literal.getBody();
+                    DCText newBody = m.at(oldBody.pos + 1).newTextTree(oldBody.getBody().substring(1));
+                    m.at(((DCTree) literal).pos);
+                    return literal.getKind() == DocTree.Kind.CODE
+                            ? m.newCodeTree(newBody)
+                            : m.newLiteralTree(newBody);
+                }
+                cx.unexpectedTree();
+                return (DCTree) literal;
+            }
+
+            @Override
+            public DCTree visitStartElement(StartElementTree node, Context cx) {
+                if (cx.state == State.BEFORE_CODE && node.getName().toString().equalsIgnoreCase("code")) {
+                    cx.state = State.AFTER_CODE;
+                } else {
+                    cx.unexpectedTree();
+                }
+                return (DCTree) node;
+            }
+
+            @Override
+            protected DCTree defaultAction(DocTree node, Context cx) {
+                cx.unexpectedTree();
+                return (DCTree) node;
+            }
+        };
+
+        Context cx = new Context();
+        var normalized = new ListBuffer();
+
+        for (var tree : trees) {
+            var visited = visitor.visit(tree, cx);
+            // null return value means the tree should be dropped
+            if (visited != null) {
+                normalized.add(visited);
+            }
+            if (cx.state == State.FAILED) {
+                return trees;
+            }
+        }
+
+        return cx.state == State.SUCCEEDED ? normalized : trees;
+    }
+
     protected void attrValueChar(ListBuffer list) {
         switch (ch) {
             case '&' -> entity(list);
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
index ac393e8a762..54c7c5cbe21 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
@@ -133,7 +133,7 @@ pre {
     line-height: var(--code-line-height);
     background-color: var(--pre-background-color);
     color: var(--pre-text-color);
-    padding: 8px;
+    padding: 10px;
     overflow-x:auto;
 }
 h1 {
diff --git a/test/langtools/jdk/javadoc/doclet/testLiteralCodeInPre/TestLiteralCodeInPre.java b/test/langtools/jdk/javadoc/doclet/testLiteralCodeInPre/TestLiteralCodeInPre.java
index ac5259686f1..1a96da79fb3 100644
--- a/test/langtools/jdk/javadoc/doclet/testLiteralCodeInPre/TestLiteralCodeInPre.java
+++ b/test/langtools/jdk/javadoc/doclet/testLiteralCodeInPre/TestLiteralCodeInPre.java
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug      8002387 8014636 8078320 8175200 8186332 8352249
+ * @bug      8002387 8014636 8078320 8175200 8186332 8352249 8352389
  * @summary  Improve rendered HTML formatting for {@code}
  * @library  ../../lib
  * @modules jdk.javadoc/jdk.javadoc.internal.tool
@@ -70,8 +70,7 @@ public class TestLiteralCodeInPre extends JavadocTester {
                 """
                     typical_usage_code()
                     
Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Example:

-                      line 0 @Override
+                    Example:  
  line 0 @Override
                       line 1 <T> void m(T t) {
                       line 2     // do something with T
                       line 3 }
@@ -80,8 +79,7 @@ public class TestLiteralCodeInPre extends JavadocTester {
                 """
                     typical_usage_literal()
Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Example:
-                      line 0 @Override
+                    Example:  
  line 0 @Override
                       line 1 <T> void m(T t) {
                       line 2     // do something with T
                       line 3 }
@@ -90,8 +88,7 @@ public class TestLiteralCodeInPre extends JavadocTester {
                 """
                     recommended_usage_literal()
Lorem ipsum dolor sit amet, consectetur adipiscing elit. - Example:
-                      line 0 @Override
+                    Example:  
  line 0 @Override
                       line 1 <T> void m(T t) {
                       line 2     // do something with T
                       line 3 } 
diff --git a/test/langtools/jdk/javadoc/doclet/testPreCode/TestPreCode.java b/test/langtools/jdk/javadoc/doclet/testPreCode/TestPreCode.java new file mode 100644 index 00000000000..c3614126aae --- /dev/null +++ b/test/langtools/jdk/javadoc/doclet/testPreCode/TestPreCode.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8352389 + * @summary Remove incidental whitespace in pre/code content + * @library /tools/lib ../../lib + * @modules jdk.javadoc/jdk.javadoc.internal.tool + * @build javadoc.tester.* toolbox.ToolBox builder.ClassBuilder + * @run main TestPreCode + */ + + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import builder.AbstractBuilder; +import builder.ClassBuilder; +import toolbox.ToolBox; + +import javadoc.tester.JavadocTester; + +public class TestPreCode extends JavadocTester { + + final ToolBox tb; + + public static void main(String... args) throws Exception { + var tester = new TestPreCode(); + tester.runTests(); + } + + TestPreCode() { + tb = new ToolBox(); + } + + @Test + public void testWhitespace(Path base) throws Exception { + Path srcDir = base.resolve("src"); + Path outDir = base.resolve("out"); + + new ClassBuilder(tb, "pkg.A") + .setComments(""" + Class A. +
 \t
+                      first line
+                      second line
+                    
""") + .setModifiers("public", "class") + .addMembers(ClassBuilder.MethodBuilder.parse("public void m0() {}") + .setComments(""" + Method m0. +
 {@code
+                                  first line
+                                  second line
+                                }
"""), + ClassBuilder.MethodBuilder.parse("public void m1() {}") + .setComments(""" + Method m1. +
  first line
+                                  second line
+                                
"""), + ClassBuilder.MethodBuilder.parse("public void m2() {}") + .setComments(""" + Method m2. +
 {@code\s
+                                  first line
+                                  second line
+                                }
"""), + ClassBuilder.MethodBuilder.parse("public void m3() {}") + .setComments(""" + Method m3. +
  .
+                                  second line
+                                
""")) + .write(srcDir); + + javadoc("-d", outDir.toString(), + "-sourcepath", srcDir.toString(), + "pkg"); + + checkExit(Exit.OK); + + checkOrder("pkg/A.html", + """ + Class A. +
  first line
+                      second line
+                    
""", + """ + Method m0. +
  first line
+                      second line
+                    
""", + """ + Method m1. +
  first line
+                      second line
+                    
""", + """ + Method m2. +
  first line
+                      second line
+                    
""", + """ + Method m3. +
  .
+                      second line
+                    
"""); + } + + @Test + public void testUnclosed(Path base) throws Exception { + Path srcDir = base.resolve("src"); + Path outDir = base.resolve("out"); + + new ClassBuilder(tb, "pkg.A") + .setComments(""" + Class A. +

+                      first line
+                      second line
+                    """)
+                .setModifiers("public", "class")
+                .write(srcDir);
+
+        javadoc("-d", outDir.toString(),
+                "-sourcepath", srcDir.toString(),
+                "pkg");
+
+        checkExit(Exit.ERROR);
+
+        // No whitespace normalization for unclosed 
 element
+        checkOrder("pkg/A.html",
+                """
+                    Class A.
+                    

+                      first line
+                      second line
+                    
"""); + } + +} diff --git a/test/langtools/tools/javac/doctree/CodeTest.java b/test/langtools/tools/javac/doctree/CodeTest.java index b7770122715..7623c41497d 100644 --- a/test/langtools/tools/javac/doctree/CodeTest.java +++ b/test/langtools/tools/javac/doctree/CodeTest.java @@ -23,7 +23,7 @@ /* * @test - * @bug 7021614 8241780 8273244 8284908 8352249 + * @bug 7021614 8241780 8273244 8284908 8352249 8352389 * @summary extend com.sun.source API to support parsing javadoc comments * @modules jdk.compiler/com.sun.tools.javac.api * jdk.compiler/com.sun.tools.javac.file @@ -128,7 +128,7 @@ DocComment[DOC_COMMENT, pos:0 name:pre attributes: empty ] - Literal[CODE, pos:5, |____@Override|____void_m()_{_}|] + Literal[CODE, pos:5, ____@Override|____void_m()_{_}|] body: 1 EndElement[END_ELEMENT, pos:44, pre] block tags: empty diff --git a/test/langtools/tools/javac/doctree/DocCommentTester.java b/test/langtools/tools/javac/doctree/DocCommentTester.java index 3591fcbca2d..1d723a2b625 100644 --- a/test/langtools/tools/javac/doctree/DocCommentTester.java +++ b/test/langtools/tools/javac/doctree/DocCommentTester.java @@ -1023,7 +1023,9 @@ public class DocCommentTester { .replaceFirst("\\.\\s*\\n *@(?![@*])", ".\n@") // Between block tags .replaceAll("\n[ \t]+@(?!([@*]|(dummy|Override)))", "\n@") .replaceAll("(?i)\\{@([a-z][a-z0-9.:-]*)\\s+}", "{@$1}") - .replaceAll("(\\{@value\\s+[^}]+)\\s+(})", "$1$2"); + .replaceAll("(\\{@value\\s+[^}]+)\\s+(})", "$1$2") + .replaceAll("
 *\\{@code\\n", "
{@code ")
+                    .replaceAll("
 *\\n", "
");
         }
     }
 
diff --git a/test/langtools/tools/javac/doctree/InPreTest.java b/test/langtools/tools/javac/doctree/InPreTest.java
index 9203f56a0f2..8f7a705c75a 100644
--- a/test/langtools/tools/javac/doctree/InPreTest.java
+++ b/test/langtools/tools/javac/doctree/InPreTest.java
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 8078320 8273244 8284908 8352249
+ * @bug 8078320 8273244 8284908 8352249 8352389
  * @summary extend com.sun.source API to support parsing javadoc comments
  * @modules jdk.compiler/com.sun.tools.javac.api
  *          jdk.compiler/com.sun.tools.javac.file
@@ -163,6 +163,50 @@ DocComment[DOC_COMMENT, pos:1
     EndElement[END_ELEMENT, pos:26, pre]
   block tags: empty
 ]
+*/
+    /**
+     * 
 {@code
+     * abc  }
+     * def
+ */ + public void in_pre_with_space_at_code_nl() { } +/* +DocComment[DOC_COMMENT, pos:0 + firstSentence: 3 + StartElement[START_ELEMENT, pos:0 + name:pre + attributes: empty + ] + Literal[CODE, pos:6, abc__] + Text[TEXT, pos:19, |def] + body: 1 + EndElement[END_ELEMENT, pos:23, pre] + block tags: empty +] +*/ + /** + *
 
+     *   abc
+     * 
+ */ + public void in_pre_with_space_code_nl() { } +/* +DocComment[DOC_COMMENT, pos:0 + firstSentence: 4 + StartElement[START_ELEMENT, pos:0 + name:pre + attributes: empty + ] + StartElement[START_ELEMENT, pos:6 + name:code + attributes: empty + ] + Text[TEXT, pos:13, __abc|] + EndElement[END_ELEMENT, pos:19, code] + body: 1 + EndElement[END_ELEMENT, pos:26, pre] + block tags: empty +] */ /** * abc {@code