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.checks.coding;
021
022import java.util.ArrayDeque;
023import java.util.Deque;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029
030/**
031 * <div>
032 * Checks that throw statements within catch blocks do not use
033 * the caught exception's getMessage() call when the thrown exception
034 * is the same type as the caught exception, as rethrowing the same
035 * exception type with its own message is redundant.
036 * </div>
037 *
038 * <p>
039 * Rationale: When throwing an exception of the same type as the caught
040 * exception and including the caught exception's message via getMessage(),
041 * the information is redundant. The original exception should be rethrown
042 * directly, or a different exception type should be used.
043 * </p>
044 *
045 * <p>
046 * Example of violations:
047 * </p>
048 * {@snippet lang="text" :
049 * catch (IOException ex) {
050 *     throw new IOException("Error: " + ex.getMessage()); // violation
051 * }
052 * }
053 *
054 * <p>
055 * Correct usage:
056 * </p>
057 * {@snippet lang="text" :
058 * catch (IOException ex) {
059 *     throw new RuntimeException("Error: " + ex.getMessage(), ex); // OK, different type
060 * }
061 * catch (IOException ex) {
062 *     throw new IOException("Error processing file", ex); // OK, no getMessage()
063 * }
064 * }
065 *
066 * @since 14.4.0
067 */
068@FileStatefulCheck
069public class NoCauseGetMessageInExceptionConstructorCheck extends AbstractCheck {
070
071    /**
072     * A key is pointing to the warning message text in "messages.properties" file.
073     */
074    public static final String MSG_KEY = "no.cause.getmessage.in.exception.constructor";
075
076    /** Stack of catch blocks currently in scope during traversal. */
077    private final Deque<DetailAST> catchScopes = new ArrayDeque<>();
078
079    /**
080     * Creates a new {@code NoCauseGetMessageInExceptionConstructorCheck} instance.
081     */
082    public NoCauseGetMessageInExceptionConstructorCheck() {
083        // no code by default
084    }
085
086    @Override
087    public int[] getDefaultTokens() {
088        return getRequiredTokens();
089    }
090
091    @Override
092    public int[] getAcceptableTokens() {
093        return getRequiredTokens();
094    }
095
096    @Override
097    public int[] getRequiredTokens() {
098        return new int[] {TokenTypes.LITERAL_THROW, TokenTypes.LITERAL_CATCH};
099    }
100
101    @Override
102    public void beginTree(DetailAST rootAST) {
103        catchScopes.clear();
104    }
105
106    @Override
107    public void visitToken(DetailAST ast) {
108        if (ast.getType() == TokenTypes.LITERAL_CATCH) {
109            catchScopes.push(ast);
110        }
111        else {
112            checkThrow(ast);
113        }
114    }
115
116    @Override
117    public void leaveToken(DetailAST ast) {
118        if (ast.getType() == TokenTypes.LITERAL_CATCH) {
119            catchScopes.pop();
120        }
121    }
122
123    /**
124     * Reports the throw statement if it references a matching caught exception's
125     * {@code getMessage()} inside its expression.
126     *
127     * @param throwAst the LITERAL_THROW AST node
128     */
129    private void checkThrow(DetailAST throwAst) {
130        final String thrownType = getThrownType(throwAst);
131        if (thrownType != null && hasViolatingGetMessage(throwAst, thrownType)) {
132            log(throwAst, MSG_KEY);
133        }
134    }
135
136    /**
137     * Gets the simple type name of the exception being thrown via a new expression.
138     *
139     * @param throwAst the LITERAL_THROW AST node
140     * @return the simple type name, or null if the throw does not use a new expression
141     */
142    private static String getThrownType(DetailAST throwAst) {
143        final DetailAST expr = throwAst.getFirstChild();
144        final DetailAST literalNew = expr.findFirstToken(TokenTypes.LITERAL_NEW);
145        String result = null;
146        if (literalNew != null) {
147            // Check for simple name (IDENT)
148            final DetailAST ident = literalNew.findFirstToken(TokenTypes.IDENT);
149            if (ident != null) {
150                result = ident.getText();
151            }
152            else {
153                // Check for qualified name (DOT)
154                final DetailAST dot = literalNew.findFirstToken(TokenTypes.DOT);
155                // Get the last part of the qualified name (the simple class name)
156                result = dot.getLastChild().getText();
157            }
158        }
159        return result;
160    }
161
162    /**
163     * Recursively checks if the AST subtree contains a {@code getMessage()} call
164     * on an enclosing caught exception whose declared type matches the thrown type.
165     *
166     * @param ast the AST node to search
167     * @param thrownType the simple type name of the thrown exception
168     * @return true if a violating call is found
169     */
170    private boolean hasViolatingGetMessage(DetailAST ast, String thrownType) {
171        boolean found = isViolatingGetMessage(ast, thrownType);
172        for (DetailAST child = ast.getFirstChild();
173                child != null && !found; child = child.getNextSibling()) {
174            found = hasViolatingGetMessage(child, thrownType);
175        }
176        return found;
177    }
178
179    /**
180     * Checks if a single node is {@code <var>.getMessage()} where {@code var} is
181     * declared by any enclosing catch whose type matches the thrown type.
182     *
183     * @param node the AST node to check
184     * @param thrownType the simple type name of the thrown exception
185     * @return true if this is a matching call
186     */
187    private boolean isViolatingGetMessage(DetailAST node, String thrownType) {
188        boolean result = false;
189        final DetailAST dot = node.findFirstToken(TokenTypes.DOT);
190        if (dot != null && "getMessage".equals(dot.getLastChild().getText())) {
191            final String varName = dot.getFirstChild().getText();
192            for (DetailAST catchBlock : catchScopes) {
193                if (matchesCatchScope(catchBlock, varName, thrownType)) {
194                    result = true;
195                    break;
196                }
197            }
198        }
199        return result;
200    }
201
202    /**
203     * Checks whether the given catch block declares the specified variable and
204     * catches a type matching the thrown type.
205     *
206     * @param catchBlock the LITERAL_CATCH AST node
207     * @param varName the caught-exception variable name to match
208     * @param thrownType the simple type name of the thrown exception
209     * @return true if the catch matches both the variable and the type
210     */
211    private static boolean matchesCatchScope(DetailAST catchBlock, String varName,
212                                             String thrownType) {
213        final DetailAST paramDef = catchBlock.findFirstToken(TokenTypes.PARAMETER_DEF);
214        final DetailAST ident = paramDef.findFirstToken(TokenTypes.IDENT);
215        return varName.equals(ident.getText())
216                && containsTypeName(paramDef, thrownType);
217    }
218
219    /**
220     * Recursively checks if the given AST contains a type matching the specified name.
221     * Handles IDENT (simple names), DOT (qualified names), and BOR (multi-catch) nodes.
222     *
223     * @param ast the AST node to search
224     * @param typeName the type name to match
225     * @return true if a matching type is found
226     */
227    private static boolean containsTypeName(DetailAST ast, String typeName) {
228        boolean result = typeName.equals(ast.getText());
229
230        for (DetailAST child = ast.getFirstChild();
231             child != null && !result; child = child.getNextSibling()) {
232            result = containsTypeName(child, typeName);
233        }
234
235        return result;
236    }
237
238}