View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.PrintWriter;
25  import java.nio.charset.StandardCharsets;
26  import java.util.function.Consumer;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
31  import com.puppycrawl.tools.checkstyle.api.DetailAST;
32  import com.puppycrawl.tools.checkstyle.api.DetailNode;
33  import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
34  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
36  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
37  import picocli.CommandLine;
38  import picocli.CommandLine.Command;
39  import picocli.CommandLine.Option;
40  import picocli.CommandLine.ParameterException;
41  import picocli.CommandLine.Parameters;
42  import picocli.CommandLine.ParseResult;
43  
44  /**
45   * This class is used internally in the build process to write a property file
46   * with short descriptions (the first sentences) of TokenTypes constants.
47   * Request: 724871
48   * For IDE plugins (like the eclipse plugin) it would be useful to have
49   * programmatic access to the first sentence of the TokenType constants,
50   * so they can use them in their configuration gui.
51   *
52   * @noinspection UseOfSystemOutOrSystemErr, unused, ClassIndependentOfModule
53   * @noinspectionreason UseOfSystemOutOrSystemErr - used for CLI output
54   * @noinspectionreason unused - main method is "unused" in code since it is driver method
55   * @noinspectionreason ClassIndependentOfModule - architecture of package requires this
56   */
57  public final class JavadocPropertiesGenerator {
58  
59      /**
60       * This regexp is used to extract the first sentence from the text.
61       * The end of the sentence is determined by the symbol "period", "exclamation mark" or
62       * "question mark", followed by a space or the end of the text.
63       */
64      private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile(
65          "(([^.?!]|[.?!](?!\\s|$))*+[.?!])(\\s|$)");
66  
67      /**
68       * Don't create instance of this class, use the {@link #main(String[])} method instead.
69       */
70      private JavadocPropertiesGenerator() {
71      }
72  
73      /**
74       * TokenTypes.properties generator entry point.
75       *
76       * @param args the command line arguments
77       * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
78       **/
79      public static void main(String... args) throws CheckstyleException {
80          final CliOptions cliOptions = new CliOptions();
81          final CommandLine cmd = new CommandLine(cliOptions);
82          try {
83              final ParseResult parseResult = cmd.parseArgs(args);
84              if (parseResult.isUsageHelpRequested()) {
85                  cmd.usage(System.out);
86              }
87              else {
88                  writePropertiesFile(cliOptions);
89              }
90          }
91          catch (ParameterException exc) {
92              System.err.println(exc.getMessage());
93              exc.getCommandLine().usage(System.err);
94          }
95      }
96  
97      /**
98       * Creates the .properties file from a .java file.
99       *
100      * @param options the user-specified options
101      * @throws CheckstyleException if a javadoc comment can not be parsed
102      */
103     private static void writePropertiesFile(CliOptions options) throws CheckstyleException {
104         try (PrintWriter writer = new PrintWriter(options.outputFile, StandardCharsets.UTF_8)) {
105             final DetailAST top = JavaParser.parseFile(options.inputFile,
106                     JavaParser.Options.WITH_COMMENTS).getFirstChild();
107             final DetailAST objBlock = getClassBody(top);
108             if (objBlock != null) {
109                 iteratePublicStaticIntFields(objBlock, writer::println);
110             }
111         }
112         catch (IOException exc) {
113             throw new CheckstyleException("Failed to write javadoc properties of '"
114                     + options.inputFile + "' to '" + options.outputFile + "'", exc);
115         }
116     }
117 
118     /**
119      * Walks over the type members and push the first javadoc sentence of every
120      * {@code public} {@code static} {@code int} field to the consumer.
121      *
122      * @param objBlock the OBJBLOCK of a class to iterate over its members
123      * @param consumer first javadoc sentence consumer
124      * @throws CheckstyleException if failed to parse a javadoc comment
125      */
126     private static void iteratePublicStaticIntFields(DetailAST objBlock, Consumer<String> consumer)
127             throws CheckstyleException {
128         for (DetailAST member = objBlock.getFirstChild(); member != null;
129                 member = member.getNextSibling()) {
130             if (isPublicStaticFinalIntField(member)) {
131                 final DetailAST modifiers = member.findFirstToken(TokenTypes.MODIFIERS);
132                 final String firstJavadocSentence = getFirstJavadocSentence(modifiers);
133                 if (firstJavadocSentence != null) {
134                     consumer.accept(getName(member) + "=" + firstJavadocSentence.trim());
135                 }
136             }
137         }
138     }
139 
140     /**
141      * Finds the class body of the first class in the DetailAST.
142      *
143      * @param top AST to find the class body
144      * @return OBJBLOCK token if found; {@code null} otherwise
145      */
146     private static DetailAST getClassBody(DetailAST top) {
147         DetailAST ast = top;
148         while (ast != null && ast.getType() != TokenTypes.CLASS_DEF) {
149             ast = ast.getNextSibling();
150         }
151         DetailAST objBlock = null;
152         if (ast != null) {
153             objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
154         }
155         return objBlock;
156     }
157 
158     /**
159      * Checks that the DetailAST is a {@code public} {@code static} {@code final} {@code int} field.
160      *
161      * @param ast to process
162      * @return {@code true} if matches; {@code false} otherwise
163      */
164     private static boolean isPublicStaticFinalIntField(DetailAST ast) {
165         boolean result = ast.getType() == TokenTypes.VARIABLE_DEF;
166         if (result) {
167             final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
168             final DetailAST arrayDeclarator = type.getFirstChild().getNextSibling();
169             result = arrayDeclarator == null
170                     && type.getFirstChild().getType() == TokenTypes.LITERAL_INT;
171             if (result) {
172                 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
173                 result = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
174                     && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
175                     && modifiers.findFirstToken(TokenTypes.FINAL) != null;
176             }
177         }
178         return result;
179     }
180 
181     /**
182      * Extracts the name of an ast.
183      *
184      * @param ast to extract the name
185      * @return the text content of the inner {@code TokenTypes.IDENT} node
186      */
187     private static String getName(DetailAST ast) {
188         return NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT)).getText();
189     }
190 
191     /**
192      * Extracts the first sentence as HTML formatted text from the comment of an DetailAST.
193      * The end of the sentence is determined by the symbol "period", "exclamation mark" or
194      * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
195      * are converted to HTML code.
196      *
197      * @param ast to extract the first sentence
198      * @return the first sentence of the inner {@code TokenTypes.BLOCK_COMMENT_BEGIN} node
199      *      or {@code null} if the first sentence is absent or malformed (does not end with period)
200      * @throws CheckstyleException if a javadoc comment can not be parsed or an unsupported inline
201      *      tag found
202      */
203     private static String getFirstJavadocSentence(DetailAST ast) throws CheckstyleException {
204         String firstSentence = null;
205         for (DetailAST child = ast.getFirstChild(); child != null && firstSentence == null;
206                 child = child.getNextSibling()) {
207             // If there is an annotation, the javadoc comment will be a child of it.
208             if (child.getType() == TokenTypes.ANNOTATION) {
209                 firstSentence = getFirstJavadocSentence(child);
210             }
211             // Otherwise, the javadoc comment will be right here.
212             else if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
213                     && JavadocUtil.isJavadocComment(child)) {
214                 final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(child);
215                 firstSentence = getFirstJavadocSentence(tree);
216             }
217         }
218         return firstSentence;
219     }
220 
221     /**
222      * Extracts the first sentence as HTML formatted text from a DetailNode.
223      * The end of the sentence is determined by the symbol "period", "exclamation mark" or
224      * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
225      * are converted to HTML code.
226      *
227      * @param tree to extract the first sentence
228      * @return the first sentence of the node or {@code null} if the first sentence is absent or
229      *      malformed (does not end with any of the end-of-sentence markers)
230      * @throws CheckstyleException if an unsupported inline tag found
231      */
232     private static String getFirstJavadocSentence(DetailNode tree) throws CheckstyleException {
233         String firstSentence = null;
234         final StringBuilder builder = new StringBuilder(128);
235 
236         for (DetailNode node = tree.getFirstChild(); node != null;
237                 node = node.getNextSibling()) {
238             if (node.getType() == JavadocCommentsTokenTypes.TEXT) {
239                 final Matcher matcher = END_OF_SENTENCE_PATTERN.matcher(node.getText());
240                 if (matcher.find()) {
241                     // Commit the sentence if an end-of-sentence marker is found.
242                     firstSentence = builder.append(matcher.group(1)).toString();
243                     break;
244                 }
245                 // Otherwise append the whole line and look for an end-of-sentence marker
246                 // on the next line.
247                 builder.append(node.getText());
248             }
249             else if (node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG) {
250                 formatInlineCodeTag(builder, node);
251             }
252             else {
253                 formatHtmlElement(builder, node);
254             }
255         }
256         return firstSentence;
257     }
258 
259     /**
260      * Converts inline code tag into HTML form.
261      *
262      * @param builder to append
263      * @param inlineTag to format
264      * @throws CheckstyleException if the inline javadoc tag is not a literal nor a code tag
265      */
266     private static void formatInlineCodeTag(StringBuilder builder, DetailNode inlineTag)
267             throws CheckstyleException {
268         final int tagType = inlineTag.getFirstChild().getType();
269 
270         if (tagType != JavadocCommentsTokenTypes.LITERAL_INLINE_TAG
271                 && tagType != JavadocCommentsTokenTypes.CODE_INLINE_TAG) {
272             throw new CheckstyleException("Unsupported inline tag "
273                 + JavadocUtil.getTokenName(tagType));
274         }
275 
276         final boolean wrapWithCodeTag = tagType == JavadocCommentsTokenTypes.CODE_INLINE_TAG;
277 
278         for (DetailNode node = inlineTag.getFirstChild().getFirstChild(); node != null;
279                 node = node.getNextSibling()) {
280             switch (node.getType()) {
281                 // The text to append.
282                 case JavadocCommentsTokenTypes.TEXT -> {
283                     if (wrapWithCodeTag) {
284                         builder.append("<code>").append(node.getText().trim()).append("</code>");
285                     }
286                     else {
287                         builder.append(node.getText().trim());
288                     }
289                 }
290                 case JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_START,
291                      JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END,
292                      JavadocCommentsTokenTypes.TAG_NAME -> {
293                     // skip tag markers
294                 }
295                 default -> throw new CheckstyleException("Unsupported child in the inline tag "
296                     + JavadocUtil.getTokenName(node.getType()));
297             }
298         }
299     }
300 
301     /**
302      * Concatenates the HTML text from AST of a JavadocTokenTypes.HTML_ELEMENT.
303      *
304      * @param builder to append
305      * @param node to format
306      */
307     private static void formatHtmlElement(StringBuilder builder, DetailNode node) {
308         switch (node.getType()) {
309             case JavadocCommentsTokenTypes.TAG_OPEN,
310                  JavadocCommentsTokenTypes.TAG_CLOSE,
311                  JavadocCommentsTokenTypes.TAG_SLASH,
312                  JavadocCommentsTokenTypes.TAG_SLASH_CLOSE,
313                  JavadocCommentsTokenTypes.TAG_NAME,
314                  JavadocCommentsTokenTypes.TEXT ->
315                 builder.append(node.getText());
316 
317             default -> {
318                 for (DetailNode child = node.getFirstChild(); child != null;
319                      child = child.getNextSibling()) {
320                     formatHtmlElement(builder, child);
321                 }
322             }
323         }
324 
325     }
326 
327     /**
328      * Helper class encapsulating the command line options and positional parameters.
329      */
330     @Command(name = "java com.puppycrawl.tools.checkstyle.JavadocPropertiesGenerator",
331             mixinStandardHelpOptions = true)
332     private static final class CliOptions {
333 
334         /**
335          * The command line option to specify the output file.
336          */
337         @Option(names = "--destfile", required = true, description = "The output file.")
338         private File outputFile;
339 
340         /**
341          * The command line positional parameter to specify the input file.
342          */
343         @Parameters(index = "0", description = "The input file.")
344         private File inputFile;
345     }
346 }