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