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