001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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    /** Max width of the usage help message for this command. */
067    private static final int USAGE_HELP_WIDTH = 100;
068
069    /**
070     * Don't create instance of this class, use the {@link #main(String[])} method instead.
071     */
072    private JavadocPropertiesGenerator() {
073    }
074
075    /**
076     * TokenTypes.properties generator entry point.
077     *
078     * @param args the command line arguments
079     * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
080     **/
081    public static void main(String... args) throws CheckstyleException {
082        final CliOptions cliOptions = new CliOptions();
083        final CommandLine cmd = new CommandLine(cliOptions).setUsageHelpWidth(USAGE_HELP_WIDTH);
084        try {
085            final ParseResult parseResult = cmd.parseArgs(args);
086            if (parseResult.isUsageHelpRequested()) {
087                cmd.usage(System.out);
088            }
089            else {
090                writePropertiesFile(cliOptions);
091            }
092        }
093        catch (ParameterException ex) {
094            System.err.println(ex.getMessage());
095            ex.getCommandLine().usage(System.err);
096        }
097    }
098
099    /**
100     * Creates the .properties file from a .java file.
101     *
102     * @param options the user-specified options
103     * @throws CheckstyleException if a javadoc comment can not be parsed
104     */
105    private static void writePropertiesFile(CliOptions options) throws CheckstyleException {
106        try (PrintWriter writer = new PrintWriter(options.outputFile, StandardCharsets.UTF_8)) {
107            final DetailAST top = JavaParser.parseFile(options.inputFile,
108                    JavaParser.Options.WITH_COMMENTS).getFirstChild();
109            final DetailAST objBlock = getClassBody(top);
110            if (objBlock != null) {
111                iteratePublicStaticIntFields(objBlock, writer::println);
112            }
113        }
114        catch (IOException ex) {
115            throw new CheckstyleException("Failed to write javadoc properties of '"
116                    + options.inputFile + "' to '" + options.outputFile + "'", ex);
117        }
118    }
119
120    /**
121     * Walks over the type members and push the first javadoc sentence of every
122     * {@code public} {@code static} {@code int} field to the consumer.
123     *
124     * @param objBlock the OBJBLOCK of a class to iterate over its members
125     * @param consumer first javadoc sentence consumer
126     * @throws CheckstyleException if failed to parse a javadoc comment
127     */
128    private static void iteratePublicStaticIntFields(DetailAST objBlock, Consumer<String> consumer)
129            throws CheckstyleException {
130        for (DetailAST member = objBlock.getFirstChild(); member != null;
131                member = member.getNextSibling()) {
132            if (isPublicStaticFinalIntField(member)) {
133                final DetailAST modifiers = member.findFirstToken(TokenTypes.MODIFIERS);
134                final String firstJavadocSentence = getFirstJavadocSentence(modifiers);
135                if (firstJavadocSentence != null) {
136                    consumer.accept(getName(member) + "=" + firstJavadocSentence.trim());
137                }
138            }
139        }
140    }
141
142    /**
143     * Finds the class body of the first class in the DetailAST.
144     *
145     * @param top AST to find the class body
146     * @return OBJBLOCK token if found; {@code null} otherwise
147     */
148    private static DetailAST getClassBody(DetailAST top) {
149        DetailAST ast = top;
150        while (ast != null && ast.getType() != TokenTypes.CLASS_DEF) {
151            ast = ast.getNextSibling();
152        }
153        DetailAST objBlock = null;
154        if (ast != null) {
155            objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
156        }
157        return objBlock;
158    }
159
160    /**
161     * Checks that the DetailAST is a {@code public} {@code static} {@code final} {@code int} field.
162     *
163     * @param ast to process
164     * @return {@code true} if matches; {@code false} otherwise
165     */
166    private static boolean isPublicStaticFinalIntField(DetailAST ast) {
167        boolean result = ast.getType() == TokenTypes.VARIABLE_DEF;
168        if (result) {
169            final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
170            final DetailAST arrayDeclarator = type.getFirstChild().getNextSibling();
171            result = arrayDeclarator == null
172                    && type.getFirstChild().getType() == TokenTypes.LITERAL_INT;
173            if (result) {
174                final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
175                result = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
176                    && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
177                    && modifiers.findFirstToken(TokenTypes.FINAL) != null;
178            }
179        }
180        return result;
181    }
182
183    /**
184     * Extracts the name of an ast.
185     *
186     * @param ast to extract the name
187     * @return the text content of the inner {@code TokenTypes.IDENT} node
188     */
189    private static String getName(DetailAST ast) {
190        return ast.findFirstToken(TokenTypes.IDENT).getText();
191    }
192
193    /**
194     * Extracts the first sentence as HTML formatted text from the comment of an DetailAST.
195     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
196     * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
197     * are converted to HTML code.
198     *
199     * @param ast to extract the first sentence
200     * @return the first sentence of the inner {@code TokenTypes.BLOCK_COMMENT_BEGIN} node
201     *      or {@code null} if the first sentence is absent or malformed (does not end with period)
202     * @throws CheckstyleException if a javadoc comment can not be parsed or an unsupported inline
203     *      tag found
204     */
205    private static String getFirstJavadocSentence(DetailAST ast) throws CheckstyleException {
206        String firstSentence = null;
207        for (DetailAST child = ast.getFirstChild(); child != null && firstSentence == null;
208                child = child.getNextSibling()) {
209            // If there is an annotation, the javadoc comment will be a child of it.
210            if (child.getType() == TokenTypes.ANNOTATION) {
211                firstSentence = getFirstJavadocSentence(child);
212            }
213            // Otherwise, the javadoc comment will be right here.
214            else if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
215                    && JavadocUtil.isJavadocComment(child)) {
216                final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(child);
217                firstSentence = getFirstJavadocSentence(tree);
218            }
219        }
220        return firstSentence;
221    }
222
223    /**
224     * Extracts the first sentence as HTML formatted text from a DetailNode.
225     * The end of the sentence is determined by the symbol "period", "exclamation mark" or
226     * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
227     * are converted to HTML code.
228     *
229     * @param tree to extract the first sentence
230     * @return the first sentence of the node or {@code null} if the first sentence is absent or
231     *      malformed (does not end with any of the end-of-sentence markers)
232     * @throws CheckstyleException if an unsupported inline tag found
233     */
234    private static String getFirstJavadocSentence(DetailNode tree) throws CheckstyleException {
235        String firstSentence = null;
236        final StringBuilder builder = new StringBuilder(128);
237        for (DetailNode node : tree.getChildren()) {
238            if (node.getType() == JavadocTokenTypes.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() == JavadocTokenTypes.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        boolean wrapWithCodeTag = false;
269        for (DetailNode node : inlineTag.getChildren()) {
270            switch (node.getType()) {
271                case JavadocTokenTypes.CODE_LITERAL:
272                    wrapWithCodeTag = true;
273                    break;
274                // The text to append.
275                case JavadocTokenTypes.TEXT:
276                    if (wrapWithCodeTag) {
277                        builder.append("<code>").append(node.getText()).append("</code>");
278                    }
279                    else {
280                        builder.append(node.getText());
281                    }
282                    break;
283                // Empty content tags.
284                case JavadocTokenTypes.LITERAL_LITERAL:
285                case JavadocTokenTypes.JAVADOC_INLINE_TAG_START:
286                case JavadocTokenTypes.JAVADOC_INLINE_TAG_END:
287                case JavadocTokenTypes.WS:
288                    break;
289                default:
290                    throw new CheckstyleException("Unsupported inline tag "
291                        + JavadocUtil.getTokenName(node.getType()));
292            }
293        }
294    }
295
296    /**
297     * Concatenates the HTML text from AST of a JavadocTokenTypes.HTML_ELEMENT.
298     *
299     * @param builder to append
300     * @param node to format
301     */
302    private static void formatHtmlElement(StringBuilder builder, DetailNode node) {
303        switch (node.getType()) {
304            case JavadocTokenTypes.START:
305            case JavadocTokenTypes.HTML_TAG_NAME:
306            case JavadocTokenTypes.END:
307            case JavadocTokenTypes.TEXT:
308            case JavadocTokenTypes.SLASH:
309                builder.append(node.getText());
310                break;
311            default:
312                for (DetailNode child : node.getChildren()) {
313                    formatHtmlElement(builder, child);
314                }
315                break;
316        }
317    }
318
319    /**
320     * Helper class encapsulating the command line options and positional parameters.
321     */
322    @Command(name = "java com.puppycrawl.tools.checkstyle.JavadocPropertiesGenerator",
323            mixinStandardHelpOptions = true)
324    private static final class CliOptions {
325
326        /**
327         * The command line option to specify the output file.
328         */
329        @Option(names = "--destfile", required = true, description = "The output file.")
330        private File outputFile;
331
332        /**
333         * The command line positional parameter to specify the input file.
334         */
335        @Parameters(index = "0", description = "The input file.")
336        private File inputFile;
337    }
338}