View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.internal;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  
24  import java.io.File;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.regex.Pattern;
32  
33  import javax.xml.parsers.ParserConfigurationException;
34  
35  import org.junit.jupiter.api.BeforeEach;
36  import org.junit.jupiter.api.Test;
37  import org.w3c.dom.Document;
38  import org.w3c.dom.NamedNodeMap;
39  import org.w3c.dom.Node;
40  import org.w3c.dom.NodeList;
41  
42  import com.google.common.base.Splitter;
43  import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
44  import com.puppycrawl.tools.checkstyle.Checker;
45  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
46  import com.puppycrawl.tools.checkstyle.ModuleFactory;
47  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
48  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
49  import com.puppycrawl.tools.checkstyle.api.DetailAST;
50  import com.puppycrawl.tools.checkstyle.api.Scope;
51  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
52  import com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocMethodCheck;
53  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
54  import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
55  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
56  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
57  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
58  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
59  
60  public class XdocsJavaDocsTest extends AbstractModuleTestSupport {
61  
62      private static final Map<String, String> CHECK_PROPERTY_DOC = new HashMap<>();
63  
64      private static Checker checker;
65  
66      private static String checkName;
67  
68      private static Path currentXdocPath;
69  
70      @Override
71      public String getPackageLocation() {
72          return "com.puppycrawl.tools.checkstyle.internal";
73      }
74  
75      @BeforeEach
76      public void setUp() throws Exception {
77          final DefaultConfiguration checkConfig = new DefaultConfiguration(
78                  JavaDocCapture.class.getName());
79          checker = createChecker(checkConfig);
80      }
81  
82      @Test
83      public void testAllCheckSectionJavaDocs() throws Exception {
84          final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
85          final List<Path> templatesWithPropertiesMacro = new ArrayList<>();
86          for (Path path : XdocUtil.getXdocsTemplatesFilePaths()) {
87              if (Files.readString(path).contains("<macro name=\"properties\">")) {
88                  templatesWithPropertiesMacro.add(path);
89              }
90          }
91  
92          for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
93              currentXdocPath = path;
94              final File file = path.toFile();
95              final String fileName = file.getName();
96  
97              if (XdocsPagesTest.isNonModulePage(fileName)
98                  || templatesWithPropertiesMacro.contains(Path.of(currentXdocPath + ".template"))) {
99                  continue;
100             }
101 
102             final String input = Files.readString(path);
103             final Document document = XmlUtil.getRawXml(fileName, input, input);
104             final NodeList sources = document.getElementsByTagName("section");
105 
106             for (int position = 0; position < sources.getLength(); position++) {
107                 final Node section = sources.item(position);
108                 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
109 
110                 if ("Content".equals(sectionName) || "Overview".equals(sectionName)
111                         || "Redirecting".equals(sectionName)) {
112                     continue;
113                 }
114 
115                 assertCheckSection(moduleFactory, fileName, sectionName);
116             }
117         }
118     }
119 
120     private static void assertCheckSection(ModuleFactory moduleFactory, String fileName,
121             String sectionName) throws Exception {
122         final Object instance;
123 
124         try {
125             instance = moduleFactory.createModule(sectionName);
126         }
127         catch (CheckstyleException exc) {
128             throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, exc);
129         }
130 
131         CHECK_PROPERTY_DOC.clear();
132         checkName = sectionName;
133 
134         final List<File> files = new ArrayList<>();
135         files.add(new File("src/main/java/" + instance.getClass().getName().replace(".", "/")
136                 + ".java"));
137 
138         checker.process(files);
139     }
140 
141     private static String getNodeText(Node node) {
142         final StringBuilder result = new StringBuilder(20);
143 
144         for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
145             if (child.getNodeType() == Node.TEXT_NODE) {
146                 for (String temp : Splitter.on("\n").split(child.getTextContent())) {
147                     final String text = temp.trim();
148 
149                     if (!text.isEmpty()) {
150                         if (shouldAppendSpace(result, text.charAt(0))) {
151                             result.append(' ');
152                         }
153 
154                         result.append(text);
155                     }
156                 }
157             }
158             else {
159                 if (child.hasAttributes() && child.getAttributes().getNamedItem("class") != null
160                         && "wrapper".equals(child.getAttributes().getNamedItem("class")
161                         .getNodeValue())) {
162                     appendNodeText(result, XmlUtil.getFirstChildElement(child));
163                 }
164                 else {
165                     appendNodeText(result, child);
166                 }
167             }
168         }
169 
170         return result.toString();
171     }
172 
173     // -@cs[CyclomaticComplexity] No simple way to split this apart.
174     private static void appendNodeText(StringBuilder result, Node node) {
175         final String name = transformXmlToJavaDocName(node.getNodeName());
176         final boolean list = "ol".equals(name) || "ul".equals(name);
177         final boolean newLineOpenBefore = list || "p".equals(name) || "pre".equals(name)
178                 || "li".equals(name);
179         final boolean newLineOpenAfter = newLineOpenBefore && !list;
180         final boolean newLineClose = newLineOpenAfter || list;
181         final boolean sanitize = "pre".equals(name);
182         final boolean changeToTag = "code".equals(name);
183 
184         if (newLineOpenBefore) {
185             result.append('\n');
186         }
187         else if (shouldAppendSpace(result, '<')) {
188             result.append(' ');
189         }
190 
191         if (changeToTag) {
192             result.append("{@")
193                     .append(name)
194                     .append(' ');
195         }
196         else {
197             result.append('<')
198                     .append(name)
199                     .append(getAttributeText(name, node.getAttributes()))
200                     .append('>');
201         }
202 
203         if (newLineOpenAfter) {
204             result.append('\n');
205         }
206 
207         if (sanitize) {
208             result.append(XmlUtil.sanitizeXml(node.getTextContent()));
209         }
210         else {
211             result.append(getNodeText(node));
212         }
213 
214         if (newLineClose) {
215             result.append('\n');
216         }
217 
218         if (changeToTag) {
219             result.append('}');
220         }
221         else {
222             result.append("</")
223                     .append(name)
224                     .append('>');
225         }
226     }
227 
228     private static boolean shouldAppendSpace(StringBuilder text, char firstCharToAppend) {
229         final boolean result;
230 
231         if (text.isEmpty()) {
232             result = false;
233         }
234         else {
235             final char last = text.charAt(text.length() - 1);
236 
237             result = (firstCharToAppend == '@'
238                     || Character.getType(firstCharToAppend) == Character.DASH_PUNCTUATION
239                     || Character.getType(last) == Character.OTHER_PUNCTUATION
240                     || Character.isAlphabetic(last)
241                     || Character.isAlphabetic(firstCharToAppend)) && !Character.isWhitespace(last);
242         }
243 
244         return result;
245     }
246 
247     private static String transformXmlToJavaDocName(String name) {
248         final String result;
249 
250         if ("source".equals(name)) {
251             result = "pre";
252         }
253         else if ("h4".equals(name)) {
254             result = "p";
255         }
256         else {
257             result = name;
258         }
259 
260         return result;
261     }
262 
263     private static String getAttributeText(String nodeName, NamedNodeMap attributes) {
264         final StringBuilder result = new StringBuilder(20);
265 
266         for (int i = 0; i < attributes.getLength(); i++) {
267             result.append(' ');
268 
269             final Node attribute = attributes.item(i);
270             final String attrName = attribute.getNodeName();
271             final String attrValue;
272 
273             if ("a".equals(nodeName) && "href".equals(attrName)) {
274                 final String value = attribute.getNodeValue();
275 
276                 assertWithMessage("links starting with '#' aren't supported: %s", value)
277                     .that(value.charAt(0))
278                     .isNotEqualTo('#');
279 
280                 attrValue = getLinkValue(value);
281             }
282             else {
283                 attrValue = attribute.getNodeValue();
284             }
285 
286             result.append(attrName)
287                     .append("=\"")
288                     .append(attrValue)
289                     .append('"');
290         }
291 
292         return result.toString();
293     }
294 
295     private static String getLinkValue(String initialValue) {
296         String value = initialValue;
297         final String attrValue;
298         if (value.contains("://")) {
299             attrValue = value;
300         }
301         else {
302             if (value.charAt(0) == '/') {
303                 value = value.substring(1);
304             }
305 
306             // Relative links to DTDs are prohibited, so we don't try to resolve them
307             if (!initialValue.startsWith("/dtds")) {
308                 value = currentXdocPath.resolveSibling(Path.of(value))
309                         .normalize()
310                         .toString()
311                         .replaceAll("src[\\\\/]site[\\\\/]xdoc[\\\\/]", "")
312                         .replaceAll("\\\\", "/");
313             }
314 
315             attrValue = "https://checkstyle.org/" + value;
316         }
317         return attrValue;
318     }
319 
320     public static class JavaDocCapture extends AbstractCheck {
321         private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
322 
323         @Override
324         public boolean isCommentNodesRequired() {
325             return true;
326         }
327 
328         @Override
329         public int[] getRequiredTokens() {
330             return new int[] {
331                 TokenTypes.BLOCK_COMMENT_BEGIN,
332             };
333         }
334 
335         @Override
336         public int[] getDefaultTokens() {
337             return getRequiredTokens();
338         }
339 
340         @Override
341         public int[] getAcceptableTokens() {
342             return getRequiredTokens();
343         }
344 
345         @Override
346         public void visitToken(DetailAST ast) {
347             if (JavadocUtil.isJavadocComment(ast)) {
348                 final DetailAST parentNode = getParent(ast);
349 
350                 switch (parentNode.getType()) {
351                     case TokenTypes.CLASS_DEF, TokenTypes.CTOR_DEF, TokenTypes.ENUM_DEF,
352                          TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.RECORD_DEF -> {
353                         // ignore
354                     }
355                     case TokenTypes.METHOD_DEF -> visitMethod(ast, parentNode);
356                     case TokenTypes.VARIABLE_DEF -> visitField(ast, parentNode);
357                     default ->
358                         assertWithMessage(
359                             "Unknown token '%s': %s", TokenUtil.getTokenName(parentNode.getType()),
360                                 ast.getLineNo()).fail();
361                 }
362             }
363         }
364 
365         private static DetailAST getParent(DetailAST node) {
366             DetailAST result = node.getParent();
367             int type = result.getType();
368 
369             while (type == TokenTypes.MODIFIERS || type == TokenTypes.ANNOTATION
370                     || type == TokenTypes.TYPE) {
371                 result = result.getParent();
372                 type = result.getType();
373             }
374 
375             return result;
376         }
377 
378         private static void visitField(DetailAST node, DetailAST parentNode) {
379             if (ScopeUtil.isInScope(parentNode, Scope.PUBLIC)) {
380                 final String propertyName = parentNode.findFirstToken(TokenTypes.IDENT).getText();
381                 final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
382 
383                 if (propertyDoc != null) {
384                     assertWithMessage("%s's class field-level JavaDoc for %s", checkName,
385                         propertyName)
386                         .that(getJavaDocText(node))
387                         .isEqualTo(makeFirstUpper(propertyDoc));
388                 }
389             }
390         }
391 
392         private static void visitMethod(DetailAST node, DetailAST parentNode) {
393             if (ScopeUtil.isInScope(node, Scope.PUBLIC) && isSetterMethod(parentNode)) {
394                 final String propertyUpper = parentNode.findFirstToken(TokenTypes.IDENT)
395                         .getText().substring(3);
396                 final String propertyName = makeFirstLower(propertyUpper);
397                 final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
398 
399                 if (propertyDoc != null) {
400                     final String javaDoc = getJavaDocText(node);
401 
402                     assertWithMessage("%s's class method-level JavaDoc for %s", checkName,
403                         propertyName)
404                         .that(javaDoc.substring(0, javaDoc.indexOf(" @param")))
405                         .isEqualTo("Setter to " + makeFirstLower(propertyDoc));
406                 }
407             }
408         }
409 
410         /**
411          * Returns whether an AST represents a setter method. This is similar to
412          * {@link MissingJavadocMethodCheck#isSetterMethod(DetailAST)} except this doesn't care
413          * about the number of children in the method.
414          *
415          * @param ast the AST to check with.
416          * @return whether the AST represents a setter method.
417          */
418         private static boolean isSetterMethod(DetailAST ast) {
419             boolean setterMethod = false;
420 
421             if (ast.getType() == TokenTypes.METHOD_DEF) {
422                 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
423                 final String name = type.getNextSibling().getText();
424                 final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches();
425                 final boolean voidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) != null;
426 
427                 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
428                 final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1;
429 
430                 if (matchesSetterFormat && voidReturnType && singleParam) {
431                     final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
432 
433                     setterMethod = slist != null;
434                 }
435             }
436             return setterMethod;
437         }
438 
439         private static String getJavaDocText(DetailAST node) {
440             final String text = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document>\n"
441                     + node.getFirstChild().getText().replaceAll("(^|\\r?\\n)\\s*\\* ?", "\n")
442                             .replaceAll("\\n?@noinspection.*\\r?\\n[^@]*", "\n")
443                             .trim() + "\n</document>";
444             String result = null;
445 
446             try {
447                 result = getNodeText(XmlUtil.getRawXml(checkName, text, text).getFirstChild())
448                         .replace("\r", "");
449             }
450             catch (ParserConfigurationException exc) {
451                 assertWithMessage("Exception: %s - %s", exc.getClass(), exc.getMessage()).fail();
452             }
453 
454             return result;
455         }
456 
457         private static String makeFirstUpper(String str) {
458             final char ch = str.charAt(0);
459             final String result;
460 
461             if (Character.isLowerCase(ch)) {
462                 result = Character.toUpperCase(ch) + str.substring(1);
463             }
464             else {
465                 result = str;
466             }
467 
468             return result;
469         }
470 
471         private static String makeFirstLower(String str) {
472             return Character.toLowerCase(str.charAt(0)) + str.substring(1);
473         }
474     }
475 
476 }