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