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