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