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.util.Set;
023
024import org.antlr.v4.runtime.BaseErrorListener;
025import org.antlr.v4.runtime.CharStreams;
026import org.antlr.v4.runtime.CommonTokenStream;
027import org.antlr.v4.runtime.RecognitionException;
028import org.antlr.v4.runtime.Recognizer;
029import org.antlr.v4.runtime.atn.PredictionMode;
030import org.antlr.v4.runtime.misc.ParseCancellationException;
031
032import com.puppycrawl.tools.checkstyle.api.DetailAST;
033import com.puppycrawl.tools.checkstyle.api.DetailNode;
034import com.puppycrawl.tools.checkstyle.grammar.SimpleToken;
035import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsLexer;
036import com.puppycrawl.tools.checkstyle.grammar.javadoc.JavadocCommentsParser;
037import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
038
039/**
040 * Used for parsing Javadoc comment as DetailNode tree.
041 *
042 */
043public class JavadocDetailNodeParser {
044
045    /**
046     * Parse error while rule recognition.
047     */
048    public static final String MSG_JAVADOC_PARSE_RULE_ERROR = "javadoc.parse.rule.error";
049
050    /**
051     * Message property key for the Unclosed HTML message.
052     */
053    public static final String MSG_UNCLOSED_HTML_TAG = "javadoc.unclosedHtml";
054
055    /** Symbols with which javadoc starts. */
056    private static final String JAVADOC_START = "/**";
057
058    /**
059     * Creates a new {@code JavadocDetailNodeParser} instance.
060     */
061    public JavadocDetailNodeParser() {
062        // no code by default
063    }
064
065    /**
066     * Parses the given Javadoc comment AST into a {@link ParseStatus} object.
067     *
068     * <p>
069     * This method extracts the raw Javadoc comment text from the supplied
070     * {@link DetailAST}, creates a new lexer and parser for the Javadoc grammar,
071     * and attempts to parse it into an AST of {@link DetailNode}s.
072     * The parser uses {@link PredictionMode#SLL} for
073     * faster performance and stops parsing on the first error encountered by
074     * using {@link CheckstyleParserErrorStrategy}.
075     * </p>
076     *
077     * @param javadocCommentAst
078     *        the {@code DetailAST} node representing the Javadoc comment in the source file
079     * @return a {@code ParseStatus} containing the root of the parsed Javadoc
080     *        tree (if successful), the first non-tight HTML tag (if any), and
081     *        the error message (if parsing failed)
082     */
083    public ParseStatus parseJavadocComment(DetailAST javadocCommentAst) {
084        final int blockCommentLineNumber = javadocCommentAst.getLineNo();
085
086        final String javadocComment = JavadocUtil.getJavadocCommentContent(javadocCommentAst);
087        final ParseStatus result = new ParseStatus();
088
089        // Use a new error listener each time to be able to use
090        // one check instance for multiple files to be checked
091        // without getting side effects.
092        final DescriptiveErrorListener errorListener = new DescriptiveErrorListener();
093
094        // Log messages should have line number in scope of file,
095        // not in scope of Javadoc comment.
096        // Offset is line number of beginning of Javadoc comment.
097        errorListener.setOffset(javadocCommentAst.getLineNo() - 1);
098
099        final JavadocCommentsLexer lexer =
100                        new JavadocCommentsLexer(CharStreams.fromString(javadocComment), true);
101
102        lexer.removeErrorListeners();
103        lexer.addErrorListener(errorListener);
104
105        final CommonTokenStream tokens = new CommonTokenStream(lexer);
106        tokens.fill();
107
108        final Set<SimpleToken> unclosedTags = lexer.getUnclosedTagNameTokens();
109        final JavadocCommentsParser parser = new JavadocCommentsParser(tokens, unclosedTags);
110
111        // set prediction mode to SLL to speed up parsing
112        parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
113
114        // remove default error listeners
115        parser.removeErrorListeners();
116
117        parser.addErrorListener(errorListener);
118
119        // JavadocParserErrorStrategy stops parsing on first parse error encountered unlike the
120        // DefaultErrorStrategy used by ANTLR which rather attempts error recovery.
121        parser.setErrorHandler(new CheckstyleParserErrorStrategy());
122
123        try {
124            final JavadocCommentsParser.JavadocContext javadoc = parser.javadoc();
125            final int javadocColumnNumber = javadocCommentAst.getColumnNo()
126                            + JAVADOC_START.length();
127
128            final JavadocCommentsAstVisitor visitor = new JavadocCommentsAstVisitor(
129                    tokens, blockCommentLineNumber, javadocColumnNumber);
130            final DetailNode tree = visitor.visit(javadoc);
131
132            result.setTree(tree);
133
134            result.firstNonTightHtmlTag = visitor.getFirstNonTightHtmlTag();
135
136            result.setParseErrorMessage(errorListener.getErrorMessage());
137        }
138        catch (ParseCancellationException | IllegalArgumentException exc) {
139            result.setParseErrorMessage(errorListener.getErrorMessage());
140        }
141
142        return result;
143    }
144
145    /**
146     * Custom error listener for JavadocParser that prints user readable errors.
147     */
148    private static final class DescriptiveErrorListener extends BaseErrorListener {
149
150        /**
151         * Offset is line number of beginning of the Javadoc comment. Log
152         * messages should have line number in scope of file, not in scope of Javadoc comment.
153         */
154        private int offset;
155
156        /**
157         * Error message that appeared while parsing.
158         */
159        private ParseErrorMessage errorMessage;
160
161        /**
162         * Creates a new {@code DescriptiveErrorListener} instance.
163         */
164        private DescriptiveErrorListener() {
165            // no code by default
166        }
167
168        /**
169         * Getter for error message during parsing.
170         *
171         * @return Error message during parsing.
172         */
173        private ParseErrorMessage getErrorMessage() {
174            return errorMessage;
175        }
176
177        /**
178         * Sets offset. Offset is line number of beginning of the Javadoc
179         * comment. Log messages should have line number in scope of file, not
180         * in scope of Javadoc comment.
181         *
182         * @param offset
183         *        offset line number
184         */
185        /* package */ void setOffset(int offset) {
186            this.offset = offset;
187        }
188
189        /**
190         * Logs parser errors in Checkstyle manner. Parser can generate error
191         * messages. There is special error that parser can generate. It is
192         * missed close HTML tag. This case is special because parser prints
193         * error like {@code "no viable alternative at input 'b \n *\n'"} and it
194         * is not clear that error is about missed close HTML tag. Other error
195         * messages are not special and logged simply as "Parse Error...".
196         *
197         * <p>{@inheritDoc}
198         */
199        @Override
200        public void syntaxError(
201                Recognizer<?, ?> recognizer, Object offendingSymbol,
202                int line, int charPositionInLine,
203                String msg, RecognitionException ex) {
204            final int lineNumber = offset + line;
205
206            final String target;
207            if (recognizer instanceof final JavadocCommentsLexer lexer) {
208                target = lexer.getPreviousToken().getText();
209            }
210            else {
211                final int ruleIndex = ex.getCtx().getRuleIndex();
212                final String ruleName = recognizer.getRuleNames()[ruleIndex];
213                target = convertUpperCamelToUpperUnderscore(ruleName);
214            }
215
216            errorMessage = new ParseErrorMessage(lineNumber,
217                    MSG_JAVADOC_PARSE_RULE_ERROR, charPositionInLine, msg, target);
218
219        }
220
221        /**
222         * Converts the given {@code text} from camel case to all upper case with
223         * underscores separating each word.
224         *
225         * @param text The string to convert.
226         * @return The result of the conversion.
227         */
228        private static String convertUpperCamelToUpperUnderscore(String text) {
229            final StringBuilder result = new StringBuilder(20);
230            for (int index = 0; index < text.length(); index++) {
231                final char letter = text.charAt(index);
232                if (Character.isUpperCase(letter)) {
233                    result.append('_');
234                }
235                result.append(Character.toUpperCase(letter));
236            }
237            return result.toString();
238        }
239
240    }
241
242    /**
243     * Contains result of parsing javadoc comment: DetailNode tree and parse error message.
244     */
245    public static class ParseStatus {
246
247        /**
248         * DetailNode tree (is null if parsing fails).
249         */
250        private DetailNode tree;
251
252        /**
253         * Parse error message (is null if parsing is successful).
254         */
255        private ParseErrorMessage parseErrorMessage;
256
257        /**
258         * Stores the first non-tight HTML tag encountered while parsing javadoc.
259         *
260         * @see <a
261         *     href="https://checkstyle.org/writing-javadoc-checks.html#Tight-HTML_rules">
262         *     Tight HTML rules</a>
263         */
264        private DetailNode firstNonTightHtmlTag;
265
266        /**
267         * Creates a new {@code ParseStatus} instance.
268         */
269        public ParseStatus() {
270            // no code by default
271        }
272
273        /**
274         * Getter for DetailNode tree.
275         *
276         * @return DetailNode tree if parsing was successful, null otherwise.
277         */
278        public DetailNode getTree() {
279            return tree;
280        }
281
282        /**
283         * Sets DetailNode tree.
284         *
285         * @param tree DetailNode tree.
286         */
287        public void setTree(DetailNode tree) {
288            this.tree = tree;
289        }
290
291        /**
292         * Getter for error message during parsing.
293         *
294         * @return Error message if parsing was unsuccessful, null otherwise.
295         */
296        public ParseErrorMessage getParseErrorMessage() {
297            return parseErrorMessage;
298        }
299
300        /**
301         * Sets parse error message.
302         *
303         * @param parseErrorMessage Parse error message.
304         */
305        public void setParseErrorMessage(ParseErrorMessage parseErrorMessage) {
306            this.parseErrorMessage = parseErrorMessage;
307        }
308
309        /**
310         * This method is used to check if the javadoc parsed has non-tight HTML tags.
311         *
312         * @return returns true if the javadoc has at least one non-tight HTML tag; false otherwise
313         * @see <a
314         *     href="https://checkstyle.org/writing-javadoc-checks.html#Tight-HTML_rules">
315         *     Tight HTML rules</a>
316         */
317        public boolean isNonTight() {
318            return firstNonTightHtmlTag != null;
319        }
320
321        /**
322         * Getter for the first non-tight HTML tag encountered while parsing javadoc.
323         *
324         * @return the first non-tight HTML tag that is encountered while parsing Javadoc,
325         *     if one exists
326         * @see <a href="https://checkstyle.org/writing-javadoc-checks.html#Tight-HTML_rules">
327         *     Tight HTML rules</a>
328         */
329        public DetailNode getFirstNonTightHtmlTag() {
330            return firstNonTightHtmlTag;
331        }
332
333    }
334
335    /**
336     * Contains information about parse error message.
337     */
338    public static class ParseErrorMessage {
339
340        /**
341         * Line number where parse error occurred.
342         */
343        private final int lineNumber;
344
345        /**
346         * Key for error message.
347         */
348        private final String messageKey;
349
350        /**
351         * Error message arguments.
352         */
353        private final Object[] messageArguments;
354
355        /**
356         * Initializes parse error message.
357         *
358         * @param lineNumber line number
359         * @param messageKey message key
360         * @param messageArguments message arguments
361         */
362        /* package */ ParseErrorMessage(int lineNumber, String messageKey,
363                Object... messageArguments) {
364            this.lineNumber = lineNumber;
365            this.messageKey = messageKey;
366            this.messageArguments = messageArguments.clone();
367        }
368
369        /**
370         * Getter for line number where parse error occurred.
371         *
372         * @return Line number where parse error occurred.
373         */
374        public int getLineNumber() {
375            return lineNumber;
376        }
377
378        /**
379         * Getter for key for error message.
380         *
381         * @return Key for error message.
382         */
383        public String getMessageKey() {
384            return messageKey;
385        }
386
387        /**
388         * Getter for error message arguments.
389         *
390         * @return Array of error message arguments.
391         */
392        public Object[] getMessageArguments() {
393            return messageArguments.clone();
394        }
395
396    }
397
398}