1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.internal;
21
22 import static java.nio.charset.StandardCharsets.UTF_8;
23
24 import java.io.File;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.util.ArrayList;
28 import java.util.Arrays;
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.Assert;
37 import org.junit.Before;
38 import org.junit.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.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.TokenTypes;
53 import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
54 import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
55 import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
56 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
57 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
58 import com.puppycrawl.tools.checkstyle.utils.ScopeUtil;
59 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
60
61 public class XdocsJavaDocsTest extends AbstractModuleTestSupport {
62 private static final List<List<Node>> CHECK_PROPERTIES = new ArrayList<>();
63 private static final Map<String, String> CHECK_PROPERTY_DOC = new HashMap<>();
64 private static final Map<String, String> CHECK_TEXT = new HashMap<>();
65
66
67
68
69
70 private static final String[] COMPATIBLE_CHECKS = {
71 "AbbreviationAsWordInName",
72 "AbstractClassName",
73 "AnnotationLocation",
74 "AnnotationOnSameLine",
75 "AnnotationUseStyle",
76 "ArrayTrailingComma",
77 "AtclauseOrder",
78 "AvoidNestedBlocks",
79 "AvoidInlineConditionals",
80 "CatchParameterName",
81 "ClassMemberImpliedModifier",
82 "ClassTypeParameterName",
83 "ConstantName",
84 "CovariantEquals",
85 "CustomImportOrder",
86 "DeclarationOrder",
87 "DefaultComesLast",
88 "EmptyBlock",
89 "EmptyCatchBlock",
90 "EmptyStatement",
91 "EqualsAvoidNull",
92 "EqualsHashCode",
93 "FallThrough",
94 "FinalLocalVariable",
95 "IllegalInstantiation",
96 "IllegalThrows",
97 "IllegalTokenText",
98 "ImportOrder",
99 "InnerAssignment",
100 "InterfaceMemberImpliedModifier",
101 "InterfaceTypeParameterName",
102 "LambdaParameterName",
103 "LeftCurly",
104 "LocalFinalVariableName",
105 "LocalVariableName",
106 "MagicNumber",
107 "MemberName",
108 "MethodName",
109 "MethodTypeParameterName",
110 "MissingCtor",
111 "MissingDeprecated",
112 "MissingJavadocType",
113 "MissingOverride",
114 "MissingSwitchDefault",
115 "NeedBraces",
116 "NoClone",
117 "PackageAnnotation",
118 "PackageDeclaration",
119 "PackageName",
120 "ParameterName",
121 "RequireThis",
122 "RightCurly",
123 "SimplifyBooleanExpression",
124 "StaticVariableName",
125 "StringLiteralEquality",
126 "SuppressWarnings",
127 "SuppressWarningsHolder",
128 "TypeName",
129 "UnnecessaryParentheses",
130 "VariableDeclarationUsageDistance",
131 };
132
133 private static Checker checker;
134
135 private static String checkName;
136
137 static {
138 Arrays.sort(COMPATIBLE_CHECKS);
139 }
140
141 @Override
142 protected String getPackageLocation() {
143 return "com.puppycrawl.tools.checkstyle.internal";
144 }
145
146 @Before
147 public void setUp() throws Exception {
148 final DefaultConfiguration checkConfig = new DefaultConfiguration(
149 JavaDocCapture.class.getName());
150 checker = createChecker(checkConfig);
151 }
152
153
154
155
156
157 @Test
158 public void testAllCheckSectionJavaDocs() throws Exception {
159 final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
160
161 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
162 final File file = path.toFile();
163 final String fileName = file.getName();
164
165 if ("config_reporting.xml".equals(fileName)) {
166 continue;
167 }
168
169 final String input = new String(Files.readAllBytes(path), UTF_8);
170 final Document document = XmlUtil.getRawXml(fileName, input, input);
171 final NodeList sources = document.getElementsByTagName("section");
172
173 for (int position = 0; position < sources.getLength(); position++) {
174 final Node section = sources.item(position);
175 final String sectionName = section.getAttributes().getNamedItem("name")
176 .getNodeValue();
177
178 if ("Content".equals(sectionName) || "Overview".equals(sectionName)
179 || Arrays.binarySearch(COMPATIBLE_CHECKS, sectionName) < 0) {
180 continue;
181 }
182
183 examineCheckSection(moduleFactory, fileName, sectionName, section);
184 }
185 }
186 }
187
188 private static void examineCheckSection(ModuleFactory moduleFactory, String fileName,
189 String sectionName, Node section) throws Exception {
190 final Object instance;
191
192 try {
193 instance = moduleFactory.createModule(sectionName);
194 }
195 catch (CheckstyleException ex) {
196 throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, ex);
197 }
198
199 CHECK_TEXT.clear();
200 CHECK_PROPERTIES.clear();
201 CHECK_PROPERTY_DOC.clear();
202 checkName = sectionName;
203
204 examineCheckSectionChildren(section);
205
206 final List<File> files = new ArrayList<>();
207 files.add(new File("src/main/java/" + instance.getClass().getName().replace(".", "/")
208 + ".java"));
209
210 checker.process(files);
211 }
212
213 private static void examineCheckSectionChildren(Node section) {
214 for (Node subSection : XmlUtil.getChildrenElements(section)) {
215 if (!"subsection".equals(subSection.getNodeName())) {
216 final String text = getNodeText(subSection, false);
217 if (text.startsWith("Since Checkstyle")) {
218 CHECK_TEXT.put("since", text.substring(17));
219 }
220 continue;
221 }
222
223 final String subSectionName = subSection.getAttributes().getNamedItem("name")
224 .getNodeValue();
225
226 examineCheckSubSection(subSection, subSectionName);
227 }
228 }
229
230 private static void examineCheckSubSection(Node subSection, String subSectionName) {
231 switch (subSectionName) {
232 case "Description":
233 case "Examples":
234 case "Notes":
235 case "Rule Description":
236 CHECK_TEXT.put(subSectionName, getNodeText(subSection, true).replace("\r", ""));
237 break;
238 case "Properties":
239 populateProperties(subSection);
240 CHECK_TEXT.put(subSectionName, createPropertiesText());
241 break;
242 case "Example of Usage":
243 break;
244 case "Error Messages":
245 break;
246 case "Package":
247 break;
248 case "Parent Module":
249 break;
250 default:
251 break;
252 }
253 }
254
255 private static void populateProperties(Node subSection) {
256 boolean skip = true;
257
258 for (Node row : XmlUtil.getChildrenElements(XmlUtil.getFirstChildElement(subSection))) {
259 if (skip) {
260 skip = false;
261 continue;
262 }
263
264 CHECK_PROPERTIES.add(new ArrayList<>(XmlUtil.getChildrenElements(row)));
265 }
266 }
267
268 private static String createPropertiesText() {
269 final StringBuilder result = new StringBuilder(100);
270
271 result.append("\n<ul>");
272
273 for (List<Node> property : CHECK_PROPERTIES) {
274 final String propertyName = getNodeText(property.get(0), true);
275
276 result.append("\n<li>\nProperty {@code ");
277 result.append(propertyName);
278 result.append("} - ");
279
280 final String temp = getNodeText(property.get(1), true);
281
282 result.append(temp);
283 CHECK_PROPERTY_DOC.put(propertyName, temp);
284
285 if (propertyName.endsWith("token") || propertyName.endsWith("tokens")) {
286 result.append(" Default value is: ");
287 }
288 else {
289 result.append(" Default value is ");
290 }
291
292 result.append(getNodeText(property.get(3), true));
293
294 if (result.charAt(result.length() - 1) != '.') {
295 result.append('.');
296 }
297
298 result.append("\n</li>");
299 }
300
301 result.append("\n</ul>");
302
303 return result.toString();
304 }
305
306 private static String getNodeText(Node node, boolean fixLinks) {
307 final StringBuilder result = new StringBuilder(20);
308
309 for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
310 if (child.getNodeType() == Node.TEXT_NODE) {
311 for (String temp : child.getTextContent().split("\n")) {
312 final String text = temp.trim();
313
314 if (!text.isEmpty()) {
315 if (shouldAppendSpace(result, text.charAt(0))) {
316 result.append(' ');
317 }
318
319 result.append(text);
320 }
321 }
322 }
323 else {
324 appendNodeText(result, child, fixLinks);
325 }
326 }
327
328 return result.toString();
329 }
330
331
332 private static void appendNodeText(StringBuilder result, Node node, boolean fixLinks) {
333 final String name = transformXmlToJavaDocName(node.getNodeName());
334 final boolean list = "ol".equals(name) || "ul".equals(name);
335 final boolean newLineOpenBefore = list || "p".equals(name) || "pre".equals(name)
336 || "li".equals(name);
337 final boolean newLineOpenAfter = newLineOpenBefore && !list;
338 final boolean newLineClose = newLineOpenAfter || list;
339 final boolean sanitize = "pre".equals(name);
340 final boolean changeToTag = "code".equals(name);
341
342 if (newLineOpenBefore) {
343 result.append('\n');
344 }
345 else if (shouldAppendSpace(result, '<')) {
346 result.append(' ');
347 }
348
349 if (changeToTag) {
350 result.append("{@");
351 result.append(name);
352 result.append(' ');
353 }
354 else {
355 result.append('<');
356 result.append(name);
357 result.append(getAttributeText(name, node.getAttributes(), fixLinks));
358 result.append('>');
359 }
360
361 if (newLineOpenAfter) {
362 result.append('\n');
363 }
364
365 if (sanitize) {
366 result.append(sanitizeXml(node.getTextContent()));
367 }
368 else {
369 result.append(getNodeText(node, fixLinks));
370 }
371
372 if (newLineClose) {
373 result.append('\n');
374 }
375
376 if (changeToTag) {
377 result.append('}');
378 }
379 else {
380 result.append("</");
381 result.append(name);
382 result.append('>');
383 }
384 }
385
386 private static boolean shouldAppendSpace(StringBuilder text, char firstCharToAppend) {
387 final boolean result;
388
389 if (text.length() == 0) {
390 result = false;
391 }
392 else {
393 final char last = text.charAt(text.length() - 1);
394
395 result = (firstCharToAppend == '@'
396 || Character.getType(last) == Character.OTHER_PUNCTUATION
397 || Character.isAlphabetic(last)
398 || Character.isAlphabetic(firstCharToAppend)) && !Character.isWhitespace(last);
399 }
400
401 return result;
402 }
403
404 private static String transformXmlToJavaDocName(String name) {
405 final String result;
406
407 if ("source".equals(name)) {
408 result = "pre";
409 }
410 else {
411 result = name;
412 }
413
414 return result;
415 }
416
417 private static String getAttributeText(String nodeName, NamedNodeMap attributes,
418 boolean fixLinks) {
419 final StringBuilder result = new StringBuilder(20);
420
421 for (int i = 0; i < attributes.getLength(); i++) {
422 result.append(' ');
423
424 final Node attribute = attributes.item(i);
425 final String attrName = attribute.getNodeName();
426 final String attrValue;
427
428 if (fixLinks && "a".equals(nodeName) && "href".equals(attrName)
429 && attribute.getNodeValue().startsWith("apidocs/")) {
430 attrValue = "https://checkstyle.org/" + attribute.getNodeValue();
431 }
432 else {
433 attrValue = attribute.getNodeValue();
434 }
435
436 result.append(attrName);
437 result.append("=\"");
438 result.append(attrValue);
439 result.append('"');
440 }
441
442 return result.toString();
443 }
444
445 private static String sanitizeXml(String nodeValue) {
446 return nodeValue.replaceAll("^[\\r\\n\\s]+", "").replaceAll("[\\r\\n\\s]+$", "")
447 .replace("<", "<").replace(">", ">");
448 }
449
450 private static class JavaDocCapture extends AbstractCheck {
451 private static final Pattern SETTER_PATTERN = Pattern.compile("^set[A-Z].*");
452
453 @Override
454 public boolean isCommentNodesRequired() {
455 return true;
456 }
457
458 @Override
459 public int[] getRequiredTokens() {
460 return new int[] {
461 TokenTypes.BLOCK_COMMENT_BEGIN,
462 };
463 }
464
465 @Override
466 public int[] getDefaultTokens() {
467 return getRequiredTokens();
468 }
469
470 @Override
471 public int[] getAcceptableTokens() {
472 return getRequiredTokens();
473 }
474
475 @Override
476 public void visitToken(DetailAST ast) {
477 if (JavadocUtil.isJavadocComment(ast)) {
478 final DetailAST node = getParent(ast);
479
480 switch (node.getType()) {
481 case TokenTypes.CLASS_DEF:
482 visitClass(ast);
483 break;
484 case TokenTypes.METHOD_DEF:
485 visitMethod(ast, node);
486 break;
487 case TokenTypes.VARIABLE_DEF:
488 final String propertyName = node.findFirstToken(TokenTypes.IDENT).getText();
489 final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
490
491 if (propertyDoc != null) {
492 Assert.assertEquals(checkName + "'s class field-level JavaDoc for "
493 + propertyName, makeFirstUpper(propertyDoc),
494 getJavaDocText(ast));
495 }
496 break;
497 case TokenTypes.CTOR_DEF:
498 case TokenTypes.ENUM_DEF:
499 case TokenTypes.ENUM_CONSTANT_DEF:
500
501 break;
502 default:
503 Assert.fail("Unknown token '" + TokenUtil.getTokenName(node.getType())
504 + "': " + ast.getLineNo());
505 break;
506 }
507 }
508 }
509
510 private static DetailAST getParent(DetailAST node) {
511 DetailAST result = node.getParent();
512 int type = result.getType();
513
514 while (type == TokenTypes.MODIFIERS || type == TokenTypes.ANNOTATION) {
515 result = result.getParent();
516 type = result.getType();
517 }
518
519 return result;
520 }
521
522 private static void visitClass(DetailAST ast) {
523 if (ScopeUtil.isInScope(ast, Scope.PUBLIC)) {
524 Assert.assertEquals(
525 checkName + "'s class-level JavaDoc",
526 CHECK_TEXT.get("Description")
527 + CHECK_TEXT.computeIfAbsent("Rule Description", unused -> "")
528 + CHECK_TEXT.computeIfAbsent("Notes", unused -> "")
529 + CHECK_TEXT.computeIfAbsent("Properties", unused -> "")
530 + CHECK_TEXT.get("Examples") + " @since "
531 + CHECK_TEXT.get("since"), getJavaDocText(ast));
532 }
533 }
534
535 private static void visitMethod(DetailAST ast, DetailAST node) {
536 if (ScopeUtil.isInScope(ast, Scope.PUBLIC) && isSetterMethod(node)) {
537 final String propertyUpper = node.findFirstToken(TokenTypes.IDENT)
538 .getText().substring(3);
539 final String propertyName = makeFirstLower(propertyUpper);
540 final String propertyDoc = CHECK_PROPERTY_DOC.get(propertyName);
541
542 if (propertyDoc != null) {
543 final String javaDoc = getJavaDocText(ast);
544
545 Assert.assertEquals(checkName + "'s class method-level JavaDoc for "
546 + propertyName,
547 "Setter to " + makeFirstLower(propertyDoc),
548 javaDoc.substring(0, javaDoc.indexOf(" @param")));
549 }
550 }
551 }
552
553
554
555
556
557
558
559
560 private static boolean isSetterMethod(DetailAST ast) {
561 boolean setterMethod = false;
562
563 if (ast.getType() == TokenTypes.METHOD_DEF) {
564 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
565 final String name = type.getNextSibling().getText();
566 final boolean matchesSetterFormat = SETTER_PATTERN.matcher(name).matches();
567 final boolean voidReturnType = type.findFirstToken(TokenTypes.LITERAL_VOID) != null;
568
569 final DetailAST params = ast.findFirstToken(TokenTypes.PARAMETERS);
570 final boolean singleParam = params.getChildCount(TokenTypes.PARAMETER_DEF) == 1;
571
572 if (matchesSetterFormat && voidReturnType && singleParam) {
573 final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
574
575 setterMethod = slist != null;
576 }
577 }
578 return setterMethod;
579 }
580
581 private static String getJavaDocText(DetailAST node) {
582 final String text = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document>\n"
583 + node.getFirstChild().getText().replaceAll("(^|\\r?\\n)\\s*\\* ?", "\n")
584 .replaceAll("\\n@noinspection.*\\r?\\n", "\n")
585 .trim() + "\n</document>";
586 String result = null;
587
588 try {
589 result = getNodeText(XmlUtil.getRawXml(checkName, text, text).getFirstChild(),
590 false).replace("\r", "");
591 }
592 catch (ParserConfigurationException ex) {
593 Assert.fail("Exception: " + ex.getClass() + " - " + ex.getMessage());
594 }
595
596 return result;
597 }
598
599 private static String makeFirstUpper(String str) {
600 final char ch = str.charAt(0);
601 final String result;
602
603 if (Character.isLowerCase(ch)) {
604 result = Character.toUpperCase(ch) + str.substring(1);
605 }
606 else {
607 result = str;
608 }
609
610 return result;
611 }
612
613 private static String makeFirstLower(String str) {
614 return Character.toLowerCase(str.charAt(0)) + str.substring(1);
615 }
616 }
617 }