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