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