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 com.puppycrawl.tools.checkstyle.StatelessCheck;
023import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.api.TokenTypes;
026
027/**
028 * <div>
029 * Checks that throw statements within catch blocks do not use
030 * the caught exception's getMessage() call when the thrown exception
031 * is the same type as the caught exception, as rethrowing the same
032 * exception type with its own message is redundant.
033 * </div>
034 *
035 * <p>
036 * Rationale: When throwing an exception of the same type as the caught
037 * exception and including the caught exception's message via getMessage(),
038 * the information is redundant. The original exception should be rethrown
039 * directly, or a different exception type should be used.
040 * </p>
041 *
042 * <p>
043 * Example of violations:
044 * </p>
045 * <div class="wrapper"><pre>
046 * catch (IOException ex) {
047 *     throw new IOException("Error: " + ex.getMessage()); // violation
048 * }
049 * </pre></div>
050 *
051 * <p>
052 * Correct usage:
053 * </p>
054 * <div class="wrapper"><pre>
055 * catch (IOException ex) {
056 *     throw new RuntimeException("Error: " + ex.getMessage(), ex); // OK, different type
057 * }
058 * catch (IOException ex) {
059 *     throw new IOException("Error processing file", ex); // OK, no getMessage()
060 * }
061 * </pre></div>
062 *
063 * @since 13.3.0
064 */
065@StatelessCheck
066public class NoGetMessageInThrowCheck extends AbstractCheck {
067
068    /**
069     * A key is pointing to the warning message text in "messages.properties" file.
070     */
071    public static final String MSG_KEY = "no.getmessage.in.throw";
072
073    @Override
074    public int[] getDefaultTokens() {
075        return getRequiredTokens();
076    }
077
078    @Override
079    public int[] getAcceptableTokens() {
080        return getRequiredTokens();
081    }
082
083    @Override
084    public int[] getRequiredTokens() {
085        return new int[] {TokenTypes.LITERAL_THROW};
086    }
087
088    @Override
089    public void visitToken(DetailAST ast) {
090        final String thrownType = getThrownType(ast);
091        if (thrownType != null && hasViolatingGetMessageCall(ast, thrownType)) {
092            log(ast, MSG_KEY);
093        }
094    }
095
096    /**
097     * Gets the simple type name of the exception being thrown via a new expression.
098     *
099     * @param throwAst the LITERAL_THROW AST node
100     * @return the simple type name, or null if the throw does not use a new expression
101     */
102    private static String getThrownType(DetailAST throwAst) {
103        final DetailAST expr = throwAst.getFirstChild();
104        final DetailAST literalNew = expr.findFirstToken(TokenTypes.LITERAL_NEW);
105        String result = null;
106        if (literalNew != null) {
107            result = literalNew.findFirstToken(TokenTypes.IDENT).getText();
108        }
109        return result;
110    }
111
112    /**
113     * Recursively checks if the AST subtree contains a getMessage() call
114     * on a caught exception variable whose type matches the thrown type.
115     *
116     * @param ast the AST node to search
117     * @param thrownType the simple type name of the thrown exception
118     * @return true if a violating getMessage() call is found
119     */
120    private static boolean hasViolatingGetMessageCall(DetailAST ast, String thrownType) {
121        boolean found = false;
122        DetailAST currentNode = ast.getFirstChild();
123
124        while (currentNode != null && !found) {
125            found = isMatchingGetMessage(currentNode, thrownType)
126                    || hasViolatingGetMessageCall(currentNode, thrownType);
127            currentNode = currentNode.getNextSibling();
128        }
129
130        return found;
131    }
132
133    /**
134     * Checks if a node represents a getMessage() call on a caught exception
135     * variable whose catch type matches the thrown type.
136     *
137     * @param node the AST node to check
138     * @param thrownType the simple type name of the thrown exception
139     * @return true if this is a matching getMessage() call
140     */
141    private static boolean isMatchingGetMessage(DetailAST node, String thrownType) {
142        final DetailAST dot = node.findFirstToken(TokenTypes.DOT);
143        boolean result = false;
144        if (dot != null) {
145            final DetailAST methodIdent = dot.getLastChild();
146            if ("getMessage".equals(methodIdent.getText())) {
147                final String varName = dot.getFirstChild().getText();
148                final DetailAST catchBlock = findCatchForVariable(node, varName);
149                if (catchBlock != null) {
150                    final DetailAST paramDef =
151                            catchBlock.findFirstToken(TokenTypes.PARAMETER_DEF);
152                    result = matchesCaughtType(paramDef, thrownType);
153                }
154            }
155        }
156        return result;
157    }
158
159    /**
160     * Walks up the AST from the given node to find an enclosing catch block
161     * that declares a parameter with the specified variable name.
162     *
163     * @param ast the starting AST node
164     * @param varName the variable name to look for
165     * @return the LITERAL_CATCH node that declares the variable, or null
166     */
167    private static DetailAST findCatchForVariable(DetailAST ast, String varName) {
168        DetailAST result = null;
169        DetailAST current = ast;
170        while (current != null) {
171            current = current.getParent();
172            if (current != null && current.getType() == TokenTypes.LITERAL_CATCH) {
173                final DetailAST paramDef =
174                        current.findFirstToken(TokenTypes.PARAMETER_DEF);
175                final DetailAST ident = paramDef.findFirstToken(TokenTypes.IDENT);
176                if (varName.equals(ident.getText())) {
177                    result = current;
178                    break;
179                }
180            }
181        }
182        return result;
183    }
184
185    /**
186     * Checks if the caught exception type in a parameter definition matches
187     * the given type name. Handles both simple types and multi-catch types.
188     *
189     * @param paramDef the PARAMETER_DEF AST node
190     * @param typeName the type name to match against
191     * @return true if the type matches any of the caught types
192     */
193    private static boolean matchesCaughtType(DetailAST paramDef, String typeName) {
194        final DetailAST typeAst = paramDef.findFirstToken(TokenTypes.TYPE);
195        return containsTypeName(typeAst, typeName);
196    }
197
198    /**
199     * Checks if the given TYPE AST's child matches the specified type name.
200     *
201     * @param ast the TYPE AST node
202     * @param typeName the type name to match
203     * @return true if the type matches
204     */
205    private static boolean containsTypeName(DetailAST ast, String typeName) {
206        return typeName.equals(ast.getFirstChild().getText());
207    }
208}