001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.PrintWriter;
025import java.nio.charset.StandardCharsets;
026import java.util.function.Consumer;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
031import com.puppycrawl.tools.checkstyle.api.DetailAST;
032import com.puppycrawl.tools.checkstyle.api.DetailNode;
033import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
034import com.puppycrawl.tools.checkstyle.api.TokenTypes;
035import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
036import picocli.CommandLine;
037import picocli.CommandLine.Command;
038import picocli.CommandLine.Option;
039import picocli.CommandLine.ParameterException;
040import picocli.CommandLine.Parameters;
041import picocli.CommandLine.ParseResult;
042
043/**
044 * This class is used internally in the build process to write a property file
045 * with short descriptions (the first sentences) of TokenTypes constants.
046 * Request: 724871
047 * For IDE plugins (like the eclipse plugin) it would be useful to have
048 * programmatic access to the first sentence of the TokenType constants,
049 * so they can use them in their configuration gui.
050 *
051 * @noinspection UseOfSystemOutOrSystemErr, unused, ClassIndependentOfModule
052 * @noinspectionreason UseOfSystemOutOrSystemErr - used for CLI output
053 * @noinspectionreason unused - main method is "unused" in code since it is driver method
054 * @noinspectionreason ClassIndependentOfModule - architecture of package requires this
055 */
056public final class JavadocPropertiesGenerator {
057
058 /**
059 * This regexp is used to extract the first sentence from the text.
060 * The end of the sentence is determined by the symbol "period", "exclamation mark" or
061 * "question mark", followed by a space or the end of the text.
062 */
063 private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile(
064 "(([^.?!]|[.?!](?!\\s|$))*+[.?!])(\\s|$)");
065
066 /**
067 * Don't create instance of this class, use the {@link #main(String[])} method instead.
068 */
069 private JavadocPropertiesGenerator() {
070 }
071
072 /**
073 * TokenTypes.properties generator entry point.
074 *
075 * @param args the command line arguments
076 * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
077 **/
078 public static void main(String... args) throws CheckstyleException {
079 final CliOptions cliOptions = new CliOptions();
080 final CommandLine cmd = new CommandLine(cliOptions);
081 try {
082 final ParseResult parseResult = cmd.parseArgs(args);
083 if (parseResult.isUsageHelpRequested()) {
084 cmd.usage(System.out);
085 }
086 else {
087 writePropertiesFile(cliOptions);
088 }
089 }
090 catch (ParameterException exc) {
091 System.err.println(exc.getMessage());
092 exc.getCommandLine().usage(System.err);
093 }
094 }
095
096 /**
097 * Creates the .properties file from a .java file.
098 *
099 * @param options the user-specified options
100 * @throws CheckstyleException if a javadoc comment can not be parsed
101 */
102 private static void writePropertiesFile(CliOptions options) throws CheckstyleException {
103 try (PrintWriter writer = new PrintWriter(options.outputFile, StandardCharsets.UTF_8)) {
104 final DetailAST top = JavaParser.parseFile(options.inputFile,
105 JavaParser.Options.WITH_COMMENTS).getFirstChild();
106 final DetailAST objBlock = getClassBody(top);
107 if (objBlock != null) {
108 iteratePublicStaticIntFields(objBlock, writer::println);
109 }
110 }
111 catch (IOException exc) {
112 throw new CheckstyleException("Failed to write javadoc properties of '"
113 + options.inputFile + "' to '" + options.outputFile + "'", exc);
114 }
115 }
116
117 /**
118 * Walks over the type members and push the first javadoc sentence of every
119 * {@code public} {@code static} {@code int} field to the consumer.
120 *
121 * @param objBlock the OBJBLOCK of a class to iterate over its members
122 * @param consumer first javadoc sentence consumer
123 * @throws CheckstyleException if failed to parse a javadoc comment
124 */
125 private static void iteratePublicStaticIntFields(DetailAST objBlock, Consumer<String> consumer)
126 throws CheckstyleException {
127 for (DetailAST member = objBlock.getFirstChild(); member != null;
128 member = member.getNextSibling()) {
129 if (isPublicStaticFinalIntField(member)) {
130 final DetailAST modifiers = member.findFirstToken(TokenTypes.MODIFIERS);
131 final String firstJavadocSentence = getFirstJavadocSentence(modifiers);
132 if (firstJavadocSentence != null) {
133 consumer.accept(getName(member) + "=" + firstJavadocSentence.trim());
134 }
135 }
136 }
137 }
138
139 /**
140 * Finds the class body of the first class in the DetailAST.
141 *
142 * @param top AST to find the class body
143 * @return OBJBLOCK token if found; {@code null} otherwise
144 */
145 private static DetailAST getClassBody(DetailAST top) {
146 DetailAST ast = top;
147 while (ast != null && ast.getType() != TokenTypes.CLASS_DEF) {
148 ast = ast.getNextSibling();
149 }
150 DetailAST objBlock = null;
151 if (ast != null) {
152 objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
153 }
154 return objBlock;
155 }
156
157 /**
158 * Checks that the DetailAST is a {@code public} {@code static} {@code final} {@code int} field.
159 *
160 * @param ast to process
161 * @return {@code true} if matches; {@code false} otherwise
162 */
163 private static boolean isPublicStaticFinalIntField(DetailAST ast) {
164 boolean result = ast.getType() == TokenTypes.VARIABLE_DEF;
165 if (result) {
166 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
167 final DetailAST arrayDeclarator = type.getFirstChild().getNextSibling();
168 result = arrayDeclarator == null
169 && type.getFirstChild().getType() == TokenTypes.LITERAL_INT;
170 if (result) {
171 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
172 result = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
173 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
174 && modifiers.findFirstToken(TokenTypes.FINAL) != null;
175 }
176 }
177 return result;
178 }
179
180 /**
181 * Extracts the name of an ast.
182 *
183 * @param ast to extract the name
184 * @return the text content of the inner {@code TokenTypes.IDENT} node
185 */
186 private static String getName(DetailAST ast) {
187 return ast.findFirstToken(TokenTypes.IDENT).getText();
188 }
189
190 /**
191 * Extracts the first sentence as HTML formatted text from the comment of an DetailAST.
192 * The end of the sentence is determined by the symbol "period", "exclamation mark" or
193 * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
194 * are converted to HTML code.
195 *
196 * @param ast to extract the first sentence
197 * @return the first sentence of the inner {@code TokenTypes.BLOCK_COMMENT_BEGIN} node
198 * or {@code null} if the first sentence is absent or malformed (does not end with period)
199 * @throws CheckstyleException if a javadoc comment can not be parsed or an unsupported inline
200 * tag found
201 */
202 private static String getFirstJavadocSentence(DetailAST ast) throws CheckstyleException {
203 String firstSentence = null;
204 for (DetailAST child = ast.getFirstChild(); child != null && firstSentence == null;
205 child = child.getNextSibling()) {
206 // If there is an annotation, the javadoc comment will be a child of it.
207 if (child.getType() == TokenTypes.ANNOTATION) {
208 firstSentence = getFirstJavadocSentence(child);
209 }
210 // Otherwise, the javadoc comment will be right here.
211 else if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
212 && JavadocUtil.isJavadocComment(child)) {
213 final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(child);
214 firstSentence = getFirstJavadocSentence(tree);
215 }
216 }
217 return firstSentence;
218 }
219
220 /**
221 * Extracts the first sentence as HTML formatted text from a DetailNode.
222 * The end of the sentence is determined by the symbol "period", "exclamation mark" or
223 * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
224 * are converted to HTML code.
225 *
226 * @param tree to extract the first sentence
227 * @return the first sentence of the node or {@code null} if the first sentence is absent or
228 * malformed (does not end with any of the end-of-sentence markers)
229 * @throws CheckstyleException if an unsupported inline tag found
230 */
231 private static String getFirstJavadocSentence(DetailNode tree) throws CheckstyleException {
232 String firstSentence = null;
233 final StringBuilder builder = new StringBuilder(128);
234 for (DetailNode node : tree.getChildren()) {
235 if (node.getType() == JavadocTokenTypes.TEXT) {
236 final Matcher matcher = END_OF_SENTENCE_PATTERN.matcher(node.getText());
237 if (matcher.find()) {
238 // Commit the sentence if an end-of-sentence marker is found.
239 firstSentence = builder.append(matcher.group(1)).toString();
240 break;
241 }
242 // Otherwise append the whole line and look for an end-of-sentence marker
243 // on the next line.
244 builder.append(node.getText());
245 }
246 else if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
247 formatInlineCodeTag(builder, node);
248 }
249 else {
250 formatHtmlElement(builder, node);
251 }
252 }
253 return firstSentence;
254 }
255
256 /**
257 * Converts inline code tag into HTML form.
258 *
259 * @param builder to append
260 * @param inlineTag to format
261 * @throws CheckstyleException if the inline javadoc tag is not a literal nor a code tag
262 */
263 private static void formatInlineCodeTag(StringBuilder builder, DetailNode inlineTag)
264 throws CheckstyleException {
265 boolean wrapWithCodeTag = false;
266 for (DetailNode node : inlineTag.getChildren()) {
267 switch (node.getType()) {
268 case JavadocTokenTypes.CODE_LITERAL:
269 wrapWithCodeTag = true;
270 break;
271 // The text to append.
272 case JavadocTokenTypes.TEXT:
273 if (wrapWithCodeTag) {
274 builder.append("<code>").append(node.getText()).append("</code>");
275 }
276 else {
277 builder.append(node.getText());
278 }
279 break;
280 // Empty content tags.
281 case JavadocTokenTypes.LITERAL_LITERAL:
282 case JavadocTokenTypes.JAVADOC_INLINE_TAG_START:
283 case JavadocTokenTypes.JAVADOC_INLINE_TAG_END:
284 case JavadocTokenTypes.WS:
285 break;
286 default:
287 throw new CheckstyleException("Unsupported inline tag "
288 + JavadocUtil.getTokenName(node.getType()));
289 }
290 }
291 }
292
293 /**
294 * Concatenates the HTML text from AST of a JavadocTokenTypes.HTML_ELEMENT.
295 *
296 * @param builder to append
297 * @param node to format
298 */
299 private static void formatHtmlElement(StringBuilder builder, DetailNode node) {
300 switch (node.getType()) {
301 case JavadocTokenTypes.START,
302 JavadocTokenTypes.HTML_TAG_NAME,
303 JavadocTokenTypes.END,
304 JavadocTokenTypes.TEXT,
305 JavadocTokenTypes.SLASH -> builder.append(node.getText());
306
307 default -> {
308 for (DetailNode child : node.getChildren()) {
309 formatHtmlElement(builder, child);
310 }
311 }
312 }
313 }
314
315 /**
316 * Helper class encapsulating the command line options and positional parameters.
317 */
318 @Command(name = "java com.puppycrawl.tools.checkstyle.JavadocPropertiesGenerator",
319 mixinStandardHelpOptions = true)
320 private static final class CliOptions {
321
322 /**
323 * The command line option to specify the output file.
324 */
325 @Option(names = "--destfile", required = true, description = "The output file.")
326 private File outputFile;
327
328 /**
329 * The command line positional parameter to specify the input file.
330 */
331 @Parameters(index = "0", description = "The input file.")
332 private File inputFile;
333 }
334}