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