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.ArrayList;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026
027import com.puppycrawl.tools.checkstyle.StatelessCheck;
028import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
029import com.puppycrawl.tools.checkstyle.api.DetailAST;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031
032/**
033 * <div>
034 * Checks that exception throw statements in catch blocks use context variables.
035 * </div>
036 *
037 * <p>
038 * Rationale: When catching an exception and throwing a new exception, it is
039 * recommended to include contextual information in the new exception constructor
040 * (such as method parameters, local variables declared outside the catch block, or fields).
041 * Failing to include context makes troubleshooting and debugging production issues
042 * significantly harder.
043 * </p>
044 *
045 * @since 14.4.0
046 */
047@StatelessCheck
048public class ExceptionThrowShouldUseContextCheck extends AbstractCheck {
049
050    /**
051     * A key is pointing to the warning message text in "messages.properties" file.
052     */
053    public static final String MSG_KEY = "exception.throw.should.use.context";
054
055    /**
056     * Creates a new {@code ExceptionThrowShouldUseContextCheck} instance.
057     */
058    public ExceptionThrowShouldUseContextCheck() {
059        // no code by default
060    }
061
062    @Override
063    public int[] getDefaultTokens() {
064        return getRequiredTokens();
065    }
066
067    @Override
068    public int[] getAcceptableTokens() {
069        return getRequiredTokens();
070    }
071
072    @Override
073    public int[] getRequiredTokens() {
074        return new int[] {
075            TokenTypes.LITERAL_CATCH,
076        };
077    }
078
079    @Override
080    public void visitToken(DetailAST ast) {
081        final DetailAST paramDef = ast.findFirstToken(TokenTypes.PARAMETER_DEF);
082        final DetailAST paramIdent = paramDef.findFirstToken(TokenTypes.IDENT);
083        final String catchParamName = paramIdent.getText();
084        final List<DetailAST> throwStatements = new ArrayList<>();
085        collectThrowStatements(ast, throwStatements);
086        for (DetailAST throwAst : throwStatements) {
087            if (isThrowWithoutContext(throwAst, catchParamName, ast)) {
088                log(throwAst, MSG_KEY);
089            }
090        }
091    }
092
093    /**
094     * Checks if a throw statement throws a new exception without using context variables.
095     *
096     * @param throwAst throw statement ast
097     * @param catchParamName name of the catch parameter
098     * @param catchAst enclosing catch ast
099     * @return true if the throw statement throws a new exception without using context variables
100     */
101    private static boolean isThrowWithoutContext(DetailAST throwAst, String catchParamName,
102                                                 DetailAST catchAst) {
103        boolean result = false;
104        final DetailAST expr = throwAst.getFirstChild();
105        final DetailAST thrownExpr = expr.getFirstChild();
106        if (thrownExpr.getType() == TokenTypes.LITERAL_NEW) {
107            final Set<String> contextVars = collectContextVariables(catchAst,
108                    catchParamName);
109            if (!contextVars.isEmpty()) {
110                final DetailAST elist = thrownExpr.findFirstToken(TokenTypes.ELIST);
111                result = !containsAnyVariable(elist, contextVars, catchParamName);
112            }
113        }
114        return result;
115    }
116
117    /**
118     * Recursively collects all throw statements in a block, skipping nested catches,
119     * classes, records, interfaces, and lambdas.
120     *
121     * @param node AST node to search
122     * @param result list to collect throw statements into
123     */
124    private static void collectThrowStatements(DetailAST node, List<DetailAST> result) {
125        for (DetailAST child = node.getFirstChild(); child != null;
126                child = child.getNextSibling()) {
127            final int type = child.getType();
128            switch (type) {
129                case TokenTypes.LITERAL_THROW -> result.add(child);
130                case TokenTypes.CLASS_DEF, TokenTypes.RECORD_DEF, TokenTypes.INTERFACE_DEF,
131                        TokenTypes.LAMBDA, TokenTypes.LITERAL_CATCH -> {
132                    // Do not descend into nested scopes
133                }
134                default -> collectThrowStatements(child, result);
135            }
136        }
137    }
138
139    /**
140     * Checks if an AST node contains any identifier from the variable set, excluding a given name.
141     *
142     * @param node AST node to search
143     * @param variables set of variable names
144     * @param excludedName identifier name to exclude
145     * @return true if any variable is found
146     */
147    private static boolean containsAnyVariable(DetailAST node, Set<String> variables,
148                                               String excludedName) {
149        boolean result = false;
150        for (DetailAST child = node.getFirstChild(); child != null && !result;
151                child = child.getNextSibling()) {
152            if (child.getType() == TokenTypes.IDENT) {
153                result = !excludedName.equals(child.getText())
154                        && variables.contains(child.getText());
155            }
156            else {
157                result = containsAnyVariable(child, variables, excludedName);
158            }
159        }
160        return result;
161    }
162
163    /**
164     * Collects all available context variables in scope: parameters, local variables outside
165     * the catch block, applicable enclosing class fields, and variables initialized with context
166     * inside the catch block.
167     *
168     * @param catchAst catch AST node
169     * @param catchParamName catch parameter name
170     * @return set of available context variable names
171     */
172    private static Set<String> collectContextVariables(DetailAST catchAst,
173                                                       String catchParamName) {
174        final Set<String> contextVars = new HashSet<>();
175        final List<DetailAST> methodScopes = findEnclosingMethodScopes(catchAst);
176        for (DetailAST methodScope : methodScopes) {
177            collectParameters(methodScope, contextVars);
178            collectLocalVariablesOutsideCatch(methodScope, catchAst, contextVars);
179        }
180        final DetailAST enclosingClass = findEnclosingClassLike(catchAst);
181        final boolean isStaticMethod = isStaticScope(methodScopes);
182        collectFields(enclosingClass, contextVars, isStaticMethod);
183        if (!isStaticMethod && isInnerClass(enclosingClass)) {
184            final DetailAST outerClass = findEnclosingClassLike(enclosingClass);
185            collectFields(outerClass, contextVars, false);
186        }
187        collectCatchVariablesWithContext(catchAst, contextVars, catchParamName);
188        return contextVars;
189    }
190
191    /**
192     * Finds all enclosing method-like scopes (methods, constructors, or lambdas).
193     *
194     * @param node starting AST node
195     * @return list of enclosing method-like AST nodes
196     */
197    private static List<DetailAST> findEnclosingMethodScopes(DetailAST node) {
198        final List<DetailAST> scopes = new ArrayList<>();
199        DetailAST current = node;
200        boolean methodScopeFound = false;
201        while (!methodScopeFound && current.getType() != TokenTypes.OBJBLOCK) {
202            final int type = current.getType();
203            switch (type) {
204                case TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF,
205                        TokenTypes.COMPACT_CTOR_DEF -> {
206                    scopes.add(current);
207                    methodScopeFound = true;
208                }
209                case TokenTypes.LAMBDA -> {
210                    scopes.add(current);
211                    current = current.getParent();
212                }
213                default -> current = current.getParent();
214            }
215        }
216        return scopes;
217    }
218
219    /**
220     * Checks if any of the enclosing method scopes is declared static.
221     *
222     * @param methodScopes method-like scopes
223     * @return true if any enclosing scope is static
224     */
225    private static boolean isStaticScope(Iterable<DetailAST> methodScopes) {
226        boolean result = false;
227        for (DetailAST scope : methodScopes) {
228            final DetailAST modifiers = scope.findFirstToken(TokenTypes.MODIFIERS);
229            if (modifiers != null && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null) {
230                result = true;
231                break;
232            }
233        }
234        return result;
235    }
236
237    /**
238     * Collects parameters from a method, constructor, or lambda.
239     *
240     * @param methodAst method-like AST node
241     * @param contextVars set to add parameter names to
242     */
243    private static void collectParameters(DetailAST methodAst, Set<String> contextVars) {
244        if (methodAst.getType() == TokenTypes.COMPACT_CTOR_DEF) {
245            collectCompactCtorParameters(methodAst, contextVars);
246        }
247        else {
248            collectStandardParameters(methodAst, contextVars);
249        }
250    }
251
252    /**
253     * Collects parameters from a compact constructor in a record.
254     *
255     * @param compactCtorDef compact constructor AST node
256     * @param contextVars set to add parameter names to
257     */
258    private static void collectCompactCtorParameters(DetailAST compactCtorDef,
259                                                     Set<String> contextVars) {
260        final DetailAST recordDef = compactCtorDef.getParent().getParent();
261        final DetailAST recordComponents =
262                recordDef.findFirstToken(TokenTypes.RECORD_COMPONENTS);
263        addRecordComponents(recordComponents, contextVars);
264    }
265
266    /**
267     * Adds record component names to context variables.
268     *
269     * @param recordComponents record components AST node
270     * @param contextVars set to add component names to
271     */
272    private static void addRecordComponents(DetailAST recordComponents,
273                                            Set<String> contextVars) {
274        for (DetailAST comp = recordComponents.getFirstChild();
275             comp != null;
276             comp = comp.getNextSibling()) {
277            if (comp.getType() == TokenTypes.RECORD_COMPONENT_DEF) {
278                final DetailAST ident = comp.findFirstToken(TokenTypes.IDENT);
279                contextVars.add(ident.getText());
280            }
281        }
282    }
283
284    /**
285     * Collects parameters from a standard method, constructor, or lambda.
286     *
287     * @param methodAst method-like AST node
288     * @param contextVars set to add parameter names to
289     */
290    private static void collectStandardParameters(DetailAST methodAst,
291                                                  Set<String> contextVars) {
292        final DetailAST parameters = methodAst.findFirstToken(TokenTypes.PARAMETERS);
293        if (parameters != null) {
294            for (DetailAST param = parameters.getFirstChild();
295                 param != null;
296                 param = param.getNextSibling()) {
297                if (param.getType() == TokenTypes.PARAMETER_DEF) {
298                    final DetailAST ident = param.findFirstToken(TokenTypes.IDENT);
299                    contextVars.add(ident.getText());
300                }
301            }
302        }
303        else {
304            final DetailAST singleParam = methodAst.findFirstToken(TokenTypes.IDENT);
305            contextVars.add(singleParam.getText());
306        }
307    }
308
309    /**
310     * Recursively collects local variables within a method, skipping the catch AST node
311     * and nested types.
312     *
313     * @param node AST node to search
314     * @param catchAst catch AST node to skip
315     * @param contextVars set to add variable names to
316     */
317    private static void collectLocalVariablesOutsideCatch(DetailAST node, DetailAST catchAst,
318                                                          Set<String> contextVars) {
319        for (DetailAST child = node.getFirstChild(); child != null;
320                child = child.getNextSibling()) {
321            if (child.equals(catchAst)) {
322                continue;
323            }
324            processLocalVariableDef(child, contextVars);
325            final int type = child.getType();
326            if (type != TokenTypes.CLASS_DEF
327                    && type != TokenTypes.RECORD_DEF
328                    && type != TokenTypes.INTERFACE_DEF) {
329                collectLocalVariablesOutsideCatch(child, catchAst, contextVars);
330            }
331        }
332    }
333
334    /**
335     * Collects variables declared within the catch block whose initializers reference
336     * an already identified context variable.
337     *
338     * @param catchAst catch AST node
339     * @param contextVars set of known context variables
340     * @param catchParamName catch parameter name
341     */
342    private static void collectCatchVariablesWithContext(DetailAST catchAst,
343                                                         Set<String> contextVars,
344                                                         String catchParamName) {
345        final DetailAST slist = catchAst.findFirstToken(TokenTypes.SLIST);
346        for (DetailAST child = slist.getFirstChild(); child != null;
347             child = child.getNextSibling()) {
348            if (child.getType() == TokenTypes.VARIABLE_DEF) {
349                final DetailAST assign = child.findFirstToken(TokenTypes.ASSIGN);
350                if (assign != null && containsAnyVariable(assign, contextVars,
351                        catchParamName)) {
352                    final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
353                    contextVars.add(ident.getText());
354                }
355            }
356        }
357    }
358
359    /**
360     * Processes a child node and adds any defined local variable name to context variables.
361     *
362     * @param child AST node to inspect
363     * @param contextVars set to add variable names to
364     */
365    private static void processLocalVariableDef(DetailAST child, Set<String> contextVars) {
366        final int type = child.getType();
367        switch (type) {
368            case TokenTypes.VARIABLE_DEF, TokenTypes.PATTERN_VARIABLE_DEF -> {
369                final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
370                contextVars.add(ident.getText());
371            }
372            case TokenTypes.RESOURCE -> {
373                final DetailAST resourceIdent = child.findFirstToken(TokenTypes.IDENT);
374                if (resourceIdent != null) {
375                    contextVars.add(resourceIdent.getText());
376                }
377            }
378            default -> {
379                // Ignore other tokens
380            }
381        }
382    }
383
384    /**
385     * Checks if an enclosing class is a non-static inner class.
386     *
387     * @param classDef class AST node
388     * @return true if the class is a non-static inner class
389     */
390    private static boolean isInnerClass(DetailAST classDef) {
391        final boolean result;
392        if (classDef.getType() == TokenTypes.CLASS_DEF) {
393            final DetailAST modifiers = classDef.findFirstToken(TokenTypes.MODIFIERS);
394            final boolean isStatic =
395                    modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
396            result = !isStatic && classDef.getParent().getType() == TokenTypes.OBJBLOCK;
397        }
398        else {
399            result = false;
400        }
401        return result;
402    }
403
404    /**
405     * Finds the nearest enclosing class, record, interface, or enum.
406     *
407     * @param node starting AST node
408     * @return enclosing type AST node, or null if none
409     */
410    private static DetailAST findEnclosingClassLike(DetailAST node) {
411        DetailAST current = node.getParent();
412        while (true) {
413            final int type = current.getType();
414            switch (type) {
415                case TokenTypes.CLASS_DEF, TokenTypes.RECORD_DEF, TokenTypes.INTERFACE_DEF,
416                        TokenTypes.ENUM_DEF, TokenTypes.COMPACT_COMPILATION_UNIT -> {
417                    return current;
418                }
419                default -> current = current.getParent();
420            }
421        }
422    }
423
424    /**
425     * Collects field names from an enclosing type's object block, filtering out instance
426     * fields when within a static scope.
427     *
428     * @param classDef enclosing type AST node
429     * @param contextVars set to add field names to
430     * @param isStaticMethod true if enclosing method is static
431     */
432    private static void collectFields(DetailAST classDef, Set<String> contextVars,
433                                      boolean isStaticMethod) {
434        final DetailAST container;
435        if (classDef.getType() == TokenTypes.COMPACT_COMPILATION_UNIT) {
436            container = classDef;
437        }
438        else {
439            container = classDef.findFirstToken(TokenTypes.OBJBLOCK);
440        }
441        for (DetailAST child = container.getFirstChild();
442             child != null;
443             child = child.getNextSibling()) {
444            if (child.getType() == TokenTypes.VARIABLE_DEF) {
445                final DetailAST modifiers = child.findFirstToken(TokenTypes.MODIFIERS);
446                final boolean isStaticField =
447                        modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null;
448                if (!isStaticMethod || isStaticField) {
449                    final DetailAST ident = child.findFirstToken(TokenTypes.IDENT);
450                    contextVars.add(ident.getText());
451                }
452            }
453        }
454    }
455
456}