001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.JavadocCommentsTokenTypes;
034import com.puppycrawl.tools.checkstyle.api.TokenTypes;
035import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
036import com.puppycrawl.tools.checkstyle.utils.NullUtil;
037import picocli.CommandLine;
038import picocli.CommandLine.Command;
039import picocli.CommandLine.Option;
040import picocli.CommandLine.ParameterException;
041import picocli.CommandLine.Parameters;
042import picocli.CommandLine.ParseResult;
043
044/**
045 * This class is used internally in the build process to write a property file
046 * with short descriptions (the first sentences) of TokenTypes constants.
047 * Request: 724871
048 * For IDE plugins (like the eclipse plugin) it would be useful to have
049 * programmatic access to the first sentence of the TokenType constants,
050 * so they can use them in their configuration gui.
051 *
052 * @noinspection UseOfSystemOutOrSystemErr, unused, ClassIndependentOfModule
053 * @noinspectionreason UseOfSystemOutOrSystemErr - used for CLI output
054 * @noinspectionreason unused - main method is "unused" in code since it is driver method
055 * @noinspectionreason ClassIndependentOfModule - architecture of package requires this
056 */
057public final class JavadocPropertiesGenerator {
058
059    /**
060     * This regexp is used to extract the first sentence from the text.
061     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
062     * "question mark", followed by a space or the end of the text.
063     */
064    private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile(
065        "(([^.?!]|[.?!](?!\\s|$))*+[.?!])(\\s|$)");
066
067    /**
068     * Don't create instance of this class, use the {@link #main(String[])} method instead.
069     */
070    private JavadocPropertiesGenerator() {
071    }
072
073    /**
074     * TokenTypes.properties generator entry point.
075     *
076     * @param args the command line arguments
077     * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
078     **/
079    public static void main(String... args) throws CheckstyleException {
080        final CliOptions cliOptions = new CliOptions();
081        final CommandLine cmd = new CommandLine(cliOptions);
082        try {
083            final ParseResult parseResult = cmd.parseArgs(args);
084            if (parseResult.isUsageHelpRequested()) {
085                cmd.usage(System.out);
086            }
087            else {
088                writePropertiesFile(cliOptions);
089            }
090        }
091        catch (ParameterException exc) {
092            System.err.println(exc.getMessage());
093            exc.getCommandLine().usage(System.err);
094        }
095    }
096
097    /**
098     * Creates the .properties file from a .java file.
099     *
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}