View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.checks.javadoc;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  import static com.puppycrawl.tools.checkstyle.checks.javadoc.WriteTagCheck.MSG_MISSING_TAG;
24  import static com.puppycrawl.tools.checkstyle.checks.javadoc.WriteTagCheck.MSG_TAG_FORMAT;
25  import static com.puppycrawl.tools.checkstyle.checks.javadoc.WriteTagCheck.MSG_WRITE_TAG;
26  
27  import java.io.ByteArrayInputStream;
28  import java.io.ByteArrayOutputStream;
29  import java.io.File;
30  import java.io.InputStreamReader;
31  import java.io.LineNumberReader;
32  import java.nio.charset.StandardCharsets;
33  import java.util.ArrayList;
34  import java.util.Collections;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  
41  import org.junit.jupiter.api.Test;
42  
43  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
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.DefaultLogger;
48  import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
49  import com.puppycrawl.tools.checkstyle.TreeWalker;
50  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
51  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
52  import de.thetaphi.forbiddenapis.SuppressForbidden;
53  
54  /**
55   * Unit test for WriteTagCheck.
56   */
57  public class WriteTagCheckTest extends AbstractModuleTestSupport {
58  
59      @Override
60      protected String getPackageLocation() {
61          return "com/puppycrawl/tools/checkstyle/checks/javadoc/writetag";
62      }
63  
64      @Test
65      public void testDefaultSettings() throws Exception {
66          final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
67          verifyWithInlineConfigParser(getPath("InputWriteTagDefault.java"), expected);
68      }
69  
70      @Test
71      public void testTag() throws Exception {
72          final String[] expected = {
73              "15: " + getCheckMessage(MSG_WRITE_TAG, "@author", "Daniel Grenner"),
74          };
75          verifyWithInlineConfigParserTwice(getPath("InputWriteTag.java"), expected);
76      }
77  
78      @Test
79      public void testMissingFormat() throws Exception {
80          final String[] expected = {
81              "15: " + getCheckMessage(MSG_WRITE_TAG, "@author", "Daniel Grenner"),
82          };
83          verifyWithInlineConfigParserTwice(
84                  getPath("InputWriteTagMissingFormat.java"), expected);
85      }
86  
87      @Test
88      public void testTagIncomplete() throws Exception {
89          final String[] expected = {
90              "16: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
91                  "This class needs more code..."),
92          };
93          verifyWithInlineConfigParserTwice(
94                  getPath("InputWriteTagIncomplete.java"), expected);
95      }
96  
97      @Test
98      public void testDoubleTag() throws Exception {
99          final String[] expected = {
100             "18: " + getCheckMessage(MSG_WRITE_TAG, "@doubletag", "first text"),
101             "19: " + getCheckMessage(MSG_WRITE_TAG, "@doubletag", "second text"),
102         };
103         verifyWithInlineConfigParserTwice(
104                 getPath("InputWriteTagDoubleTag.java"), expected);
105     }
106 
107     @Test
108     public void testEmptyTag() throws Exception {
109         final String[] expected = {
110             "19: " + getCheckMessage(MSG_WRITE_TAG, "@emptytag", ""),
111         };
112         verifyWithInlineConfigParserTwice(
113                 getPath("InputWriteTagEmptyTag.java"), expected);
114     }
115 
116     @Test
117     public void testMissingTag() throws Exception {
118         final String[] expected = {
119             "20: " + getCheckMessage(MSG_MISSING_TAG, "@missingtag"),
120         };
121         verifyWithInlineConfigParserTwice(
122                 getPath("InputWriteTagMissingTag.java"), expected);
123     }
124 
125     @Test
126     public void testMethod() throws Exception {
127         final String[] expected = {
128             "24: " + getCheckMessage(MSG_WRITE_TAG, "@todo",
129                     "Add a constructor comment"),
130             "36: " + getCheckMessage(MSG_WRITE_TAG, "@todo", "Add a comment"),
131         };
132         verifyWithInlineConfigParserTwice(
133                 getPath("InputWriteTagMethod.java"), expected);
134     }
135 
136     @Test
137     public void testSeverity() throws Exception {
138         final String[] expected = {
139             "16: " + getCheckMessage(MSG_WRITE_TAG, "@author", "Daniel Grenner"),
140         };
141         verifyWithInlineConfigParserTwice(
142                 getPath("InputWriteTagSeverity.java"), expected);
143     }
144 
145     /**
146      * Reason for low level testing:
147      * There is no direct way to fetch severity level directly.
148      * This is an exceptional case in which the logs are fetched indirectly using default
149      * logger listener in order to check for the severity level being reset by logging twice.
150      * First log should be the tag's severity level then it should reset the severity level back to
151      * error which is then checked upon using the log's severity level.
152      * This test needs to use a forbidden api {@code ByteArrayOutputStream#toString()}
153      * to get the logs as a string from the output stream
154      */
155     @Test
156     @SuppressForbidden
157     public void testResetSeverityLevel() throws Exception {
158 
159         final Checker checker = new Checker();
160 
161         final TreeWalker treeWalker = new TreeWalker();
162         final PackageObjectFactory factory = new PackageObjectFactory(
163                 new HashSet<>(), Thread.currentThread().getContextClassLoader());
164 
165         treeWalker.setModuleFactory(factory);
166         treeWalker.finishLocalSetup();
167 
168         final DefaultConfiguration writeTagConfig = createModuleConfig(WriteTagCheck.class);
169         writeTagConfig.addProperty("tag", "@author");
170         writeTagConfig.addProperty("tagFormat", "Mohanad");
171         writeTagConfig.addProperty("tagSeverity", "warning");
172 
173         treeWalker.setupChild(writeTagConfig);
174 
175         checker.addFileSetCheck(treeWalker);
176 
177         final ByteArrayOutputStream out = TestUtil.getInternalState(this, "stream");
178         final DefaultLogger logger = new DefaultLogger(out,
179                 AbstractAutomaticBean.OutputStreamOptions.CLOSE);
180         checker.addListener(logger);
181 
182         execute(checker, getPath("InputWriteTagResetSeverity.java"));
183 
184         final String output = out.toString();
185 
186         // logs severity levels are between square brackets []
187         final Pattern severityPattern = Pattern.compile("\\[(ERROR|WARN|INFO|IGNORE)]");
188 
189         final Matcher matcher = severityPattern.matcher(output);
190 
191         // First log is just the normal tag one
192         final boolean firstMatchFound = matcher.find();
193         assertWithMessage("Severity level should be wrapped in a square bracket []")
194                 .that(firstMatchFound)
195                 .isTrue();
196 
197         final String tagExpectedSeverityLevel = "warn";
198         final String firstSeverityLevel = matcher.group(1).toLowerCase(Locale.ENGLISH);
199 
200         assertWithMessage("First log should have an error severity level")
201                 .that(firstSeverityLevel)
202                 .isEqualTo(tagExpectedSeverityLevel);
203 
204         // Now we check for the second log which should log error  if
205         // the previous log did not have an issue while resetting the original severity level
206         final boolean secondMatchFound = matcher.find();
207         assertWithMessage("Severity level should be wrapped in a square bracket []")
208                 .that(secondMatchFound)
209                 .isTrue();
210 
211         final String expectedSeverityLevelAfterReset = "error";
212 
213         final String secondSeverityLevel = matcher.group(1).toLowerCase(Locale.ENGLISH);
214 
215         assertWithMessage("Second violation's severity level"
216                 + " should have been reset back to default (error)")
217                 .that(secondSeverityLevel)
218                 .isEqualTo(expectedSeverityLevelAfterReset);
219 
220     }
221 
222     @Test
223     public void testIgnoreMissing() throws Exception {
224         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
225         verifyWithInlineConfigParserTwice(getPath("InputWriteTagIgnore.java"), expected);
226     }
227 
228     @Test
229     public void testRegularEx() throws Exception {
230         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
231         verifyWithInlineConfigParserTwice(
232                 getPath("InputWriteTagRegularExpression.java"), expected);
233     }
234 
235     @Test
236     public void testRegularExError() throws Exception {
237         final String[] expected = {
238             "15: " + getCheckMessage(MSG_TAG_FORMAT, "@author", "ABC"),
239         };
240         verifyWithInlineConfigParserTwice(
241                 getPath("InputWriteTagExpressionError.java"), expected);
242     }
243 
244     @Test
245     public void testEnumsAndAnnotations() throws Exception {
246         final String[] expected = {
247             "16: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
248                     "This enum needs more code..."),
249             "21: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
250                     "This enum constant needs more code..."),
251             "28: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
252                     "This annotation needs more code..."),
253             "33: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
254                     "This annotation field needs more code..."),
255         };
256         verifyWithInlineConfigParserTwice(
257                 getPath("InputWriteTagEnumsAndAnnotations.java"), expected);
258     }
259 
260     @Test
261     public void testNoJavadocs() throws Exception {
262         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
263 
264         verifyWithInlineConfigParserTwice(getPath("InputWriteTagNoJavadoc.java"), expected);
265     }
266 
267     @Test
268     public void testWriteTagRecordsAndCompactCtors() throws Exception {
269         final String[] expected = {
270             "19: " + getCheckMessage(MSG_TAG_FORMAT, "@incomplete", "\\S"),
271             "26: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
272                     "Failed to recognize 'record' introduced in Java 14."),
273             "37: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
274                     "Failed to recognize 'record' introduced in Java 14."),
275             "48: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
276                     "Failed to recognize 'record' introduced in Java 14."),
277             "62: " + getCheckMessage(MSG_WRITE_TAG, "@incomplete",
278                     "Failed to recognize 'record' introduced in Java 14."),
279         };
280         verifyWithInlineConfigParserTwice(
281             getNonCompilablePath("InputWriteTagRecordsAndCompactCtors.java"), expected);
282     }
283 
284     @Override
285     protected void verify(Checker checker,
286                           File[] processedFiles,
287                           String messageFileName,
288                           String... expected)
289             throws Exception {
290         getStream().flush();
291         final List<File> theFiles = new ArrayList<>();
292         Collections.addAll(theFiles, processedFiles);
293         final int errs = checker.process(theFiles);
294 
295         // process each of the lines
296         try (ByteArrayInputStream localStream =
297                 new ByteArrayInputStream(getStream().toByteArray());
298             LineNumberReader lnr = new LineNumberReader(
299                 new InputStreamReader(localStream, StandardCharsets.UTF_8))) {
300             for (int i = 0; i < expected.length; i++) {
301                 final String expectedResult = messageFileName + ":" + expected[i];
302                 final String actual = lnr.readLine();
303                 assertWithMessage("error message " + i)
304                         .that(actual)
305                         .isEqualTo(expectedResult);
306             }
307 
308             assertWithMessage("unexpected output: " + lnr.readLine())
309                     .that(errs)
310                     .isAtMost(expected.length);
311         }
312         checker.destroy();
313     }
314 
315 }