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