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  import static com.puppycrawl.tools.checkstyle.site.SiteUtil.SINCE_VERSION;
24  
25  import java.io.File;
26  import java.net.URI;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.regex.Pattern;
34  
35  import javax.xml.parsers.ParserConfigurationException;
36  
37  import org.junit.jupiter.api.BeforeEach;
38  import org.junit.jupiter.api.Test;
39  import org.w3c.dom.Document;
40  import org.w3c.dom.NamedNodeMap;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  
44  import com.google.common.collect.ImmutableMap;
45  import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
46  import com.puppycrawl.tools.checkstyle.Checker;
47  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
48  import com.puppycrawl.tools.checkstyle.ModuleFactory;
49  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
50  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
51  import com.puppycrawl.tools.checkstyle.api.DetailAST;
52  import com.puppycrawl.tools.checkstyle.api.Scope;
53  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
54  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
55  import com.puppycrawl.tools.checkstyle.checks.LineSeparatorOption;
56  import com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationUseStyleCheck;
57  import com.puppycrawl.tools.checkstyle.checks.blocks.BlockOption;
58  import com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyOption;
59  import com.puppycrawl.tools.checkstyle.checks.blocks.RightCurlyOption;
60  import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderOption;
61  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocContentLocationOption;
62  import com.puppycrawl.tools.checkstyle.checks.javadoc.MissingJavadocMethodCheck;
63  import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
64  import com.puppycrawl.tools.checkstyle.checks.whitespace.PadOption;
65  import com.puppycrawl.tools.checkstyle.checks.whitespace.WrapOption;
66  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
67  import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
68  import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
69  import com.puppycrawl.tools.checkstyle.site.PropertiesMacro;
70  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
71  import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
72  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
73  
74  public class XdocsJavaDocsTest extends AbstractModuleTestSupport {
75      private static final Map<String, Class<?>> FULLY_QUALIFIED_CLASS_NAMES =
76              ImmutableMap.<String, Class<?>>builder()
77              .put("int", int.class)
78              .put("int[]", int[].class)
79              .put("boolean", boolean.class)
80              .put("double", double.class)
81              .put("double[]", double[].class)
82              .put("String", String.class)
83              .put("String[]", String[].class)
84              .put("Pattern", Pattern.class)
85              .put("Pattern[]", Pattern[].class)
86              .put("AccessModifierOption[]", AccessModifierOption[].class)
87              .put("BlockOption", BlockOption.class)
88              .put("ClosingParensOption", AnnotationUseStyleCheck.ClosingParensOption.class)
89              .put("ElementStyleOption", AnnotationUseStyleCheck.ElementStyleOption.class)
90              .put("File", File.class)
91              .put("ImportOrderOption", ImportOrderOption.class)
92              .put("JavadocContentLocationOption", JavadocContentLocationOption.class)
93              .put("LeftCurlyOption", LeftCurlyOption.class)
94              .put("LineSeparatorOption", LineSeparatorOption.class)
95              .put("PadOption", PadOption.class)
96              .put("RightCurlyOption", RightCurlyOption.class)
97              .put("Scope", Scope.class)
98              .put("SeverityLevel", SeverityLevel.class)
99              .put("TrailingArrayCommaOption", AnnotationUseStyleCheck.TrailingArrayCommaOption.class)
100             .put("URI", URI.class)
101             .put("WrapOption", WrapOption.class)
102             .put("PARAM_LITERAL", int[].class).build();
103 
104     private static final List<List<Node>> CHECK_PROPERTIES = new ArrayList<>();
105     private static final Map<String, String> CHECK_PROPERTY_DOC = new HashMap<>();
106     private static final Map<String, String> CHECK_TEXT = new HashMap<>();
107 
108     private static Checker checker;
109 
110     private static String checkName;
111 
112     private static Path currentXdocPath;
113 
114     @Override
115     protected String getPackageLocation() {
116         return "com.puppycrawl.tools.checkstyle.internal";
117     }
118 
119     @BeforeEach
120     public void setUp() throws Exception {
121         final DefaultConfiguration checkConfig = new DefaultConfiguration(
122                 JavaDocCapture.class.getName());
123         checker = createChecker(checkConfig);
124     }
125 
126     @Test
127     public void testAllCheckSectionJavaDocs() throws Exception {
128         final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
129 
130         for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
131             currentXdocPath = path;
132             final File file = path.toFile();
133             final String fileName = file.getName();
134 
135             if (XdocsPagesTest.isNonModulePage(fileName)) {
136                 continue;
137             }
138 
139             final String input = Files.readString(path);
140             final Document document = XmlUtil.getRawXml(fileName, input, input);
141             final NodeList sources = document.getElementsByTagName("section");
142 
143             for (int position = 0; position < sources.getLength(); position++) {
144                 final Node section = sources.item(position);
145                 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
146 
147                 if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
148                     continue;
149                 }
150 
151                 assertCheckSection(moduleFactory, fileName, sectionName, section);
152             }
153         }
154     }
155 
156     private static void assertCheckSection(ModuleFactory moduleFactory, String fileName,
157             String sectionName, Node section) throws Exception {
158         final Object instance;
159 
160         try {
161             instance = moduleFactory.createModule(sectionName);
162         }
163         catch (CheckstyleException exc) {
164             throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, exc);
165         }
166 
167         CHECK_TEXT.clear();
168         CHECK_PROPERTIES.clear();
169         CHECK_PROPERTY_DOC.clear();
170         checkName = sectionName;
171 
172         assertCheckSectionChildren(section);
173 
174         final List<File> files = new ArrayList<>();
175         files.add(new File("src/main/java/" + instance.getClass().getName().replace(".", "/")
176                 + ".java"));
177 
178         checker.process(files);
179     }
180 
181     private static void assertCheckSectionChildren(Node section) {
182         for (Node subSection : XmlUtil.getChildrenElements(section)) {
183             if (!"subsection".equals(subSection.getNodeName())) {
184                 final String text = getNodeText(subSection);
185                 if (text.startsWith("Since Checkstyle")) {
186                     CHECK_TEXT.put("since", text.substring(17));
187                 }
188                 continue;
189             }
190 
191             final String subSectionName = XmlUtil.getNameAttributeOfNode(subSection);
192 
193             examineCheckSubSection(subSection, subSectionName);
194         }
195     }
196 
197     private static void examineCheckSubSection(Node subSection, String subSectionName) {
198         switch (subSectionName) {
199             case "Description":
200             case "Examples":
201             case "Notes":
202             case "Rule Description":
203                 CHECK_TEXT.put(subSectionName, getNodeText(subSection).replace("\r", ""));
204                 break;
205             case "Properties":
206                 populateProperties(subSection);
207                 CHECK_TEXT.put(subSectionName, createPropertiesText());
208                 break;
209             case "Example of Usage":
210             case "Violation Messages":
211                 CHECK_TEXT.put(subSectionName,
212                         createViolationMessagesText(getViolationMessages(subSection)));
213                 break;
214             case "Package":
215             case "Parent Module":
216                 CHECK_TEXT.put(subSectionName, createParentText(subSection));
217                 break;
218             default:
219                 break;
220         }
221     }
222 
223     private static List<String> getViolationMessages(Node subsection) {
224         final Node child = XmlUtil.getFirstChildElement(subsection);
225         final List<String> violationMessages = new ArrayList<>();
226         for (Node row : XmlUtil.getChildrenElements(child)) {
227             violationMessages.add(row.getTextContent().trim());
228         }
229         return violationMessages;
230     }
231 
232     private static String createViolationMessagesText(List<String> violationMessages) {
233         final StringBuilder result = new StringBuilder(100);
234         result.append("\n<p>\nViolation Message Keys:\n</p>\n<ul>");
235 
236         for (String msg : violationMessages) {
237             result.append("\n<li>\n{@code ").append(msg).append("}\n</li>");
238         }
239 
240         result.append("\n</ul>");
241         return result.toString();
242     }
243 
244     private static String createParentText(Node subsection) {
245         return "\n<p>\nParent is {@code com.puppycrawl.tools.checkstyle."
246                 + XmlUtil.getFirstChildElement(subsection).getTextContent().trim() + "}\n</p>";
247     }
248 
249     private static void populateProperties(Node subSection) {
250         boolean skip = true;
251 
252         // if the first child is a wrapper element instead of the first table row containing
253         // the table headset
254         //   element to populate properties for to the current elements first child
255         Node child = XmlUtil.getFirstChildElement(subSection);
256         if (child.hasAttributes() && child.getAttributes().getNamedItem("class") != null
257                 && "wrapper".equals(child.getAttributes().getNamedItem("class")
258                 .getTextContent())) {
259             child = XmlUtil.getFirstChildElement(child);
260         }
261         for (Node row : XmlUtil.getChildrenElements(child)) {
262             if (skip) {
263                 skip = false;
264                 continue;
265             }
266             CHECK_PROPERTIES.add(new ArrayList<>(XmlUtil.getChildrenElements(row)));
267         }
268     }
269 
270     private static String createPropertiesText() {
271         final StringBuilder result = new StringBuilder(100);
272 
273         result.append("\n<ul>");
274 
275         for (List<Node> property : CHECK_PROPERTIES) {
276             final String propertyName = getNodeText(property.get(0));
277 
278             result.append("\n<li>\nProperty {@code ");
279             result.append(propertyName);
280             result.append("} - ");
281 
282             final String temp = getNodeText(property.get(1));
283 
284             result.append(temp);
285             CHECK_PROPERTY_DOC.put(propertyName, temp);
286 
287             String typeText = "java.lang.String[]";
288             final String propertyType = property.get(2).getTextContent();
289             final boolean isSpecialAllTokensType = propertyType.contains("set of any supported");
290             final boolean isPropertyTokenType = isSpecialAllTokensType
291                     || propertyType.contains("subset of tokens")
292                     || propertyType.contains("subset of javadoc tokens");
293             if (!isPropertyTokenType) {
294                 final String typeName =
295                         getCorrectNodeBasedOnPropertyType(property).getTextContent().trim();
296                 typeText = FULLY_QUALIFIED_CLASS_NAMES.get(typeName).getTypeName();
297             }
298             if (isSpecialAllTokensType) {
299                 typeText = "anyTokenTypesSet";
300             }
301             result.append(" Type is {@code ").append(typeText).append("}.");
302 
303             if (!isSpecialAllTokensType) {
304                 final String validationType = getValidationType(isPropertyTokenType, propertyName);
305                 if (validationType != null) {
306                     result.append(validationType);
307                 }
308             }
309 
310             result.append(getDefaultValueOfType(propertyName, isSpecialAllTokensType));
311 
312             result.append(emptyStringArrayDefaultValue(property.get(3), isPropertyTokenType));
313 
314             if (result.charAt(result.length() - 1) != '.') {
315                 result.append('.');
316             }
317 
318             result.append("\n</li>");
319         }
320 
321         result.append("\n</ul>");
322 
323         return result.toString();
324     }
325 
326     private static Node getCorrectNodeBasedOnPropertyType(List<Node> property) {
327         final Node result;
328         if (property.get(2).getFirstChild().getFirstChild() == null) {
329             result = property.get(2).getFirstChild().getNextSibling();
330         }
331         else {
332             result = property.get(2).getFirstChild().getFirstChild();
333         }
334         return result;
335     }
336 
337     private static String getDefaultValueOfType(String propertyName,
338                                                 boolean isSpecialAllTokensType) {
339         final String result;
340         if (!isSpecialAllTokensType
341                 && (propertyName.endsWith("token") || propertyName.endsWith(
342                 "tokens"))) {
343             result = " Default value is: ";
344         }
345         else {
346             result = " Default value is ";
347         }
348         return result;
349     }
350 
351     private static String getValidationType(boolean isPropertyTokenType, String propertyName) {
352         String result = null;
353         if (PropertiesMacro.NON_BASE_TOKEN_PROPERTIES.contains(checkName + " - " + propertyName)) {
354             result = " Validation type is {@code tokenTypesSet}.";
355         }
356         else if (isPropertyTokenType) {
357             result = " Validation type is {@code tokenSet}.";
358         }
359         return result;
360     }
361 
362     private static String emptyStringArrayDefaultValue(Node defaultValueNode,
363                                                 boolean isPropertyTokenType) {
364         String defaultValueText = getNodeText(defaultValueNode);
365         if ("{@code {}}".equals(defaultValueText)
366             || "{@code all files}".equals(defaultValueText)
367             || isPropertyTokenType && "{@code empty}".equals(defaultValueText)) {
368             defaultValueText = "{@code \"\"}";
369         }
370         return defaultValueText;
371     }
372 
373     private static String getNodeText(Node node) {
374         final StringBuilder result = new StringBuilder(20);
375 
376         for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
377             if (child.getNodeType() == Node.TEXT_NODE) {
378                 for (String temp : child.getTextContent().split("\n")) {
379                     final String text = temp.trim();
380 
381                     if (!text.isEmpty()) {
382                         if (shouldAppendSpace(result, text.charAt(0))) {
383                             result.append(' ');
384                         }
385 
386                         result.append(text);
387                     }
388                 }
389             }
390             else {
391                 if (child.hasAttributes() && child.getAttributes().getNamedItem("class") != null
392                         && "wrapper".equals(child.getAttributes().getNamedItem("class")
393                         .getNodeValue())) {
394                     appendNodeText(result, XmlUtil.getFirstChildElement(child));
395                 }
396                 else {
397                     appendNodeText(result, child);
398                 }
399             }
400         }
401 
402         return result.toString();
403     }
404 
405     // -@cs[CyclomaticComplexity] No simple way to split this apart.
406     private static void appendNodeText(StringBuilder result, Node node) {
407         final String name = transformXmlToJavaDocName(node.getNodeName());
408         final boolean list = "ol".equals(name) || "ul".equals(name);
409         final boolean newLineOpenBefore = list || "p".equals(name) || "pre".equals(name)
410                 || "li".equals(name);
411         final boolean newLineOpenAfter = newLineOpenBefore && !list;
412         final boolean newLineClose = newLineOpenAfter || list;
413         final boolean sanitize = "pre".equals(name);
414         final boolean changeToTag = "code".equals(name);
415 
416         if (newLineOpenBefore) {
417             result.append('\n');
418         }
419         else if (shouldAppendSpace(result, '<')) {
420             result.append(' ');
421         }
422 
423         if (changeToTag) {
424             result.append("{@");
425             result.append(name);
426             result.append(' ');
427         }
428         else {
429             result.append('<');
430             result.append(name);
431             result.append(getAttributeText(name, node.getAttributes()));
432             result.append('>');
433         }
434 
435         if (newLineOpenAfter) {
436             result.append('\n');
437         }
438 
439         if (sanitize) {
440             result.append(XmlUtil.sanitizeXml(node.getTextContent()));
441         }
442         else {
443             result.append(getNodeText(node));
444         }
445 
446         if (newLineClose) {
447             result.append('\n');
448         }
449 
450         if (changeToTag) {
451             result.append('}');
452         }
453         else {
454             result.append("</");
455             result.append(name);
456             result.append('>');
457         }
458     }
459 
460     private static boolean shouldAppendSpace(StringBuilder text, char firstCharToAppend) {
461         final boolean result;
462 
463         if (text.length() == 0) {
464             result = false;
465         }
466         else {
467             final char last = text.charAt(text.length() - 1);
468 
469             result = (firstCharToAppend == '@'
470                     || Character.getType(firstCharToAppend) == Character.DASH_PUNCTUATION
471                     || Character.getType(last) == Character.OTHER_PUNCTUATION
472                     || Character.isAlphabetic(last)
473                     || Character.isAlphabetic(firstCharToAppend)) && !Character.isWhitespace(last);
474         }
475 
476         return result;
477     }
478 
479     private static String transformXmlToJavaDocName(String name) {
480         final String result;
481 
482         if ("source".equals(name)) {
483             result = "pre";
484         }
485         else if ("h4".equals(name)) {
486             result = "p";
487         }
488         else {
489             result = name;
490         }
491 
492         return result;
493     }
494 
495     private static String getAttributeText(String nodeName, NamedNodeMap attributes) {
496         final StringBuilder result = new StringBuilder(20);
497 
498         for (int i = 0; i < attributes.getLength(); i++) {
499             result.append(' ');
500 
501             final Node attribute = attributes.item(i);
502             final String attrName = attribute.getNodeName();
503             final String attrValue;
504 
505             if ("a".equals(nodeName) && "href".equals(attrName)) {
506                 final String value = attribute.getNodeValue();
507 
508                 assertWithMessage("links starting with '#' aren't supported: " + value)
509                     .that(value.charAt(0))
510                     .isNotEqualTo('#');
511 
512                 attrValue = getLinkValue(value);
513             }
514             else {
515                 attrValue = attribute.getNodeValue();
516             }
517 
518             result.append(attrName);
519             result.append("=\"");
520             result.append(attrValue);
521             result.append('"');
522         }
523 
524         return result.toString();
525     }
526 
527     private static String getLinkValue(String initialValue) {
528         String value = initialValue;
529         final String attrValue;
530         if (value.contains("://")) {
531             attrValue = value;
532         }
533         else {
534             if (value.charAt(0) == '/') {
535                 value = value.substring(1);
536             }
537 
538             // Relative links to DTDs are prohibited, so we don't try to resolve them
539             if (!initialValue.startsWith("/dtds")) {
540                 value = currentXdocPath
541                         .getParent()
542                         .resolve(Path.of(value))
543                         .normalize()
544                         .toString()
545                         .replaceAll("src[\\\\/]site[\\\\/]xdoc[\\\\/]", "")
546                         .replaceAll("\\\\", "/");
547             }
548 
549             attrValue = "https://checkstyle.org/" + value;
550         }
551         return attrValue;
552     }
553 
554     public static class JavaDocCapture extends AbstractCheck {
555         private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
556 
557         @Override
558         public boolean isCommentNodesRequired() {
559             return true;
560         }
561 
562         @Override
563         public int[] getRequiredTokens() {
564             return new int[] {
565                 TokenTypes.BLOCK_COMMENT_BEGIN,
566             };
567         }
568 
569         @Override
570         public int[] getDefaultTokens() {
571             return getRequiredTokens();
572         }
573 
574         @Override
575         public int[] getAcceptableTokens() {
576             return getRequiredTokens();
577         }
578 
579         @Override
580         public void visitToken(DetailAST ast) {
581             if (JavadocUtil.isJavadocComment(ast)) {
582                 final DetailAST parentNode = getParent(ast);
583 
584                 switch (parentNode.getType()) {
585                     case TokenTypes.CLASS_DEF:
586                         visitClass(ast);
587                         break;
588                     case TokenTypes.METHOD_DEF:
589                         visitMethod(ast, parentNode);
590                         break;
591                     case TokenTypes.VARIABLE_DEF:
592                         visitField(ast, parentNode);
593                         break;
594                     case TokenTypes.CTOR_DEF:
595                     case TokenTypes.ENUM_DEF:
596                     case TokenTypes.ENUM_CONSTANT_DEF:
597                         // ignore
598                         break;
599                     default:
600                         assertWithMessage(
601                                 "Unknown token '" + TokenUtil.getTokenName(parentNode.getType())
602                                         + "': " + ast.getLineNo()).fail();
603                         break;
604                 }
605             }
606         }
607 
608         private static DetailAST getParent(DetailAST node) {
609             DetailAST result = node.getParent();
610             int type = result.getType();
611 
612             while (type == TokenTypes.MODIFIERS || type == TokenTypes.ANNOTATION) {
613                 result = result.getParent();
614                 type = result.getType();
615             }
616 
617             return result;
618         }
619 
620         private static void visitClass(DetailAST node) {
621             String violationMessagesText = CHECK_TEXT.get("Violation Messages");
622 
623             if (checkName.endsWith("Filter") || "SuppressWarningsHolder".equals(checkName)) {
624                 violationMessagesText = "";
625             }
626 
627             if (ScopeUtil.isInScope(node, Scope.PUBLIC)) {
628                 final String expected = CHECK_TEXT.get("Description")
629                         + CHECK_TEXT.computeIfAbsent("Rule Description", unused -> "")
630                         + CHECK_TEXT.computeIfAbsent("Notes", unused -> "")
631                         + CHECK_TEXT.computeIfAbsent("Properties", unused -> "")
632                         + CHECK_TEXT.get("Parent Module")
633                         + violationMessagesText + " @since "
634                         + CHECK_TEXT.get("since");
635 
636                 assertWithMessage(checkName + "'s class-level JavaDoc")
637                     .that(getJavaDocText(node))
638                     .isEqualTo(expected);
639             }
640         }
641 
642         private static void visitField(DetailAST node, DetailAST parentNode) {
643             if (ScopeUtil.isInScope(parentNode, Scope.PUBLIC)) {
644                 final String propertyName = parentNode.findFirstToken(TokenTypes.IDENT).getText();
645                 final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
646 
647                 if (propertyDoc != null) {
648                     assertWithMessage(checkName + "'s class field-level JavaDoc for "
649                             + propertyName)
650                         .that(getJavaDocText(node))
651                         .isEqualTo(makeFirstUpper(propertyDoc));
652                 }
653             }
654         }
655 
656         private static void visitMethod(DetailAST node, DetailAST parentNode) {
657             if (ScopeUtil.isInScope(node, Scope.PUBLIC) && isSetterMethod(parentNode)) {
658                 final String propertyUpper = parentNode.findFirstToken(TokenTypes.IDENT)
659                         .getText().substring(3);
660                 final String propertyName = makeFirstLower(propertyUpper);
661                 final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
662 
663                 if (propertyDoc != null) {
664                     final String javaDoc = getJavaDocText(node);
665 
666                     assertWithMessage(checkName + "'s class method-level JavaDoc for "
667                             + propertyName)
668                         .that(javaDoc.substring(0, javaDoc.indexOf(" @param")))
669                         .isEqualTo("Setter to " + makeFirstLower(propertyDoc));
670                 }
671             }
672         }
673 
674         /**
675          * Returns whether an AST represents a setter method. This is similar to
676          * {@link MissingJavadocMethodCheck#isSetterMethod(DetailAST)} except this doesn't care
677          * about the number of children in the method.
678          *
679          * @param ast the AST to check with.
680          * @return whether the AST represents a setter method.
681          */
682         private static boolean isSetterMethod(DetailAST ast) {
683             boolean setterMethod = false;
684 
685             if (ast.getType() == TokenTypes.METHOD_DEF) {
686                 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
687                 final String name = type.getNextSibling().getText();
688                 final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches();
689                 final boolean voidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) != null;
690 
691                 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
692                 final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1;
693 
694                 if (matchesSetterFormat && voidReturnType && singleParam) {
695                     final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
696 
697                     setterMethod = slist != null;
698                 }
699             }
700             return setterMethod;
701         }
702 
703         private static String getJavaDocText(DetailAST node) {
704             String text = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document>\n"
705                     + node.getFirstChild().getText().replaceAll("(^|\\r?\\n)\\s*\\* ?", "\n")
706                             .replaceAll("\\n?@noinspection.*\\r?\\n[^@]*", "\n")
707                             .trim() + "\n</document>";
708             String result = null;
709 
710             // until https://github.com/checkstyle/checkstyle/issues/17251
711             if (text.contains("\n" + SINCE_VERSION)) {
712                 final String sinceVersionLine = "\n" + SINCE_VERSION + " .*";
713                 text = text.replaceAll(sinceVersionLine, "");
714             }
715 
716             try {
717                 result = getNodeText(XmlUtil.getRawXml(checkName, text, text).getFirstChild())
718                         .replace("\r", "");
719             }
720             catch (ParserConfigurationException exc) {
721                 assertWithMessage("Exception: " + exc.getClass() + " - " + exc.getMessage()).fail();
722             }
723 
724             return result;
725         }
726 
727         private static String makeFirstUpper(String str) {
728             final char ch = str.charAt(0);
729             final String result;
730 
731             if (Character.isLowerCase(ch)) {
732                 result = Character.toUpperCase(ch) + str.substring(1);
733             }
734             else {
735                 result = str;
736             }
737 
738             return result;
739         }
740 
741         private static String makeFirstLower(String str) {
742             return Character.toLowerCase(str.charAt(0)) + str.substring(1);
743         }
744     }
745 }