From c4081e1820761aa6daa71fd40084f168163c6a85 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:25:32 +0000 Subject: [PATCH 1/6] Initial plan From eb217e8a8696e9940b9c6a8a9f5ebcdae0b0ffb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:41:25 +0000 Subject: [PATCH 2/6] Fix SC2317 false positive on return ||: when script is not sourced Co-authored-by: dotysan <5060170+dotysan@users.noreply.github.com> --- src/ShellCheck/Analytics.hs | 3 +++ src/ShellCheck/CFG.hs | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index a4e2186a4..822064726 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -5049,6 +5049,9 @@ prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz" prop_checkCommandIsUnreachable4 = verifyNot checkCommandIsUnreachable "f() { foo; }; # Maybe sourced" prop_checkCommandIsUnreachable5 = verify checkCommandIsUnreachable "f() { foo; }; exit # Not sourced" +prop_checkCommandIsUnreachable6 = verifyNot checkCommandIsUnreachable "return || :; echo 'reachable'" +prop_checkCommandIsUnreachable7 = verifyNot checkCommandIsUnreachable "return; echo 'reachable when not in function'" +prop_checkCommandIsUnreachable8 = verify checkCommandIsUnreachable "f() { return; echo unreachable; }" checkCommandIsUnreachable params t = case t of T_Pipeline {} -> sequence_ $ do diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs index c235cb7d4..7d6e3d87b 100644 --- a/src/ShellCheck/CFG.hs +++ b/src/ShellCheck/CFG.hs @@ -983,13 +983,20 @@ handleCommand cmd vars args literalCmd = do handleReturn = do returnTarget <- reader cfReturnTarget - case returnTarget of - Nothing -> error $ pleaseReport "missing return target" - Just target -> do + isFunction <- reader cfIsFunction + case (returnTarget, isFunction) of + (Nothing, _) -> error $ pleaseReport "missing return target" + (Just target, True) -> do + -- In a function: return actually returns, making subsequent code unreachable ret <- newNode CFStructuralNode link ret target CFEFlow unreachable <- newNode CFUnreachable return $ Range ret unreachable + (Just _, False) -> do + -- Not in a function: return will fail and execution continues + -- Treat it like a regular command that sets exit code + ret <- newNodeRange $ CFSetExitCode (getId cmd) + return ret handleUnset (cmd NE.:| args) = do case () of From 0a186fd2e5f59164634680d372bb5479ef10e7df Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Fri, 2 Jan 2026 23:07:06 +0000 Subject: [PATCH 3/6] Add 2nd unit test variant --- src/ShellCheck/Analytics.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 822064726..05b0fbd84 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -5049,9 +5049,10 @@ prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz" prop_checkCommandIsUnreachable4 = verifyNot checkCommandIsUnreachable "f() { foo; }; # Maybe sourced" prop_checkCommandIsUnreachable5 = verify checkCommandIsUnreachable "f() { foo; }; exit # Not sourced" -prop_checkCommandIsUnreachable6 = verifyNot checkCommandIsUnreachable "return || :; echo 'reachable'" -prop_checkCommandIsUnreachable7 = verifyNot checkCommandIsUnreachable "return; echo 'reachable when not in function'" -prop_checkCommandIsUnreachable8 = verify checkCommandIsUnreachable "f() { return; echo unreachable; }" +prop_checkCommandIsUnreachable6 = verifyNot checkCommandIsUnreachable "return || true; echo 'reachable'" +prop_checkCommandIsUnreachable7 = verifyNot checkCommandIsUnreachable "return 2>/dev/null ||:" +prop_checkCommandIsUnreachable8 = verifyNot checkCommandIsUnreachable "return; echo 'reachable when not in function'" +prop_checkCommandIsUnreachable9 = verify checkCommandIsUnreachable "f() { return; echo unreachable; }" checkCommandIsUnreachable params t = case t of T_Pipeline {} -> sequence_ $ do From 57739bc996758e6677bd7e19607a3e0b0ac22366 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:49:43 +0000 Subject: [PATCH 4/6] Initial plan From 9d0236487df867bdc809851d9a4dd43ea5fb3b0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 02:12:50 +0000 Subject: [PATCH 5/6] Fix SC2329 false positive when function is invoked before definition with exit $? Co-authored-by: dotysan <5060170+dotysan@users.noreply.github.com> --- src/ShellCheck/Analytics.hs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 05b0fbd84..bbca17aeb 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -5053,6 +5053,8 @@ prop_checkCommandIsUnreachable6 = verifyNot checkCommandIsUnreachable "return || prop_checkCommandIsUnreachable7 = verifyNot checkCommandIsUnreachable "return 2>/dev/null ||:" prop_checkCommandIsUnreachable8 = verifyNot checkCommandIsUnreachable "return; echo 'reachable when not in function'" prop_checkCommandIsUnreachable9 = verify checkCommandIsUnreachable "f() { return; echo unreachable; }" +prop_checkCommandIsUnreachable10 = verifyNot checkCommandIsUnreachable "f; f() { :; }; exit $?" +prop_checkCommandIsUnreachable11 = verifyNot checkCommandIsUnreachable "PS4func; PS4func() { echo test; }; exit $?" checkCommandIsUnreachable params t = case t of T_Pipeline {} -> sequence_ $ do @@ -5062,10 +5064,11 @@ checkCommandIsUnreachable params t = guard . not $ isSourced params t guard . not $ any (\t -> isUnreachable t || isUnreachableFunction t) $ NE.drop 1 $ getPath (parentMap params) t return $ info (getId t) 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." - T_Function id _ _ _ _ -> + T_Function id _ _ name _ -> when (isUnreachableFunction t && (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t) - && (not $ isSourced params t)) $ + && (not $ isSourced params t) + && (not $ isFunctionInvokedInReachableCode name)) $ info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)." _ -> return () where @@ -5078,6 +5081,18 @@ checkCommandIsUnreachable params t = cfga <- cfgAnalysis params state <- CF.getIncomingState cfga (getId t) return . not $ CF.stateIsReachable state + + -- Check if a function is invoked anywhere in reachable code + isFunctionInvokedInReachableCode :: String -> Bool + isFunctionInvokedInReachableCode name = + any isFunctionCall $ analyse findCalls (rootNode params) + where + findCalls token = when (isFunctionCall token) $ modify (token:) + isFunctionCall token = + case token of + T_SimpleCommand _ _ (cmd:_) -> + getUnquotedLiteral cmd == Just name && not (isUnreachable token) + _ -> False prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]" From 5fc9fbe33d1c5978dd436f8a044521aae0188856 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Jan 2026 03:20:50 +0000 Subject: [PATCH 6/6] Fix SC2329 false positive for functions in prompt variables and command substitutions Correctly recognize function references in: - Command substitutions $() and backticks - Prompt variables (PS1, PS2, PS3, PS4, PROMPT_COMMAND) Functions referenced in these contexts are considered invoked even if the function definition appears unreachable in the CFG. Co-authored-by: dotysan <5060170+dotysan@users.noreply.github.com> --- src/ShellCheck/Analytics.hs | 48 ++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index bbca17aeb..474ed688b 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -5053,8 +5053,8 @@ prop_checkCommandIsUnreachable6 = verifyNot checkCommandIsUnreachable "return || prop_checkCommandIsUnreachable7 = verifyNot checkCommandIsUnreachable "return 2>/dev/null ||:" prop_checkCommandIsUnreachable8 = verifyNot checkCommandIsUnreachable "return; echo 'reachable when not in function'" prop_checkCommandIsUnreachable9 = verify checkCommandIsUnreachable "f() { return; echo unreachable; }" -prop_checkCommandIsUnreachable10 = verifyNot checkCommandIsUnreachable "f; f() { :; }; exit $?" -prop_checkCommandIsUnreachable11 = verifyNot checkCommandIsUnreachable "PS4func; PS4func() { echo test; }; exit $?" +prop_checkCommandIsUnreachable10 = verifyNot checkCommandIsUnreachable "PS4func() { :; }; PS4='$(PS4func)'; exit" +prop_checkCommandIsUnreachable11 = verifyNot checkCommandIsUnreachable "f() { :; }; var=`f`; exit" checkCommandIsUnreachable params t = case t of T_Pipeline {} -> sequence_ $ do @@ -5068,7 +5068,7 @@ checkCommandIsUnreachable params t = when (isUnreachableFunction t && (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t) && (not $ isSourced params t) - && (not $ isFunctionInvokedInReachableCode name)) $ + && (not $ isFunctionReferencedInCommandSubstitution name)) $ info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)." _ -> return () where @@ -5082,17 +5082,43 @@ checkCommandIsUnreachable params t = state <- CF.getIncomingState cfga (getId t) return . not $ CF.stateIsReachable state - -- Check if a function is invoked anywhere in reachable code - isFunctionInvokedInReachableCode :: String -> Bool - isFunctionInvokedInReachableCode name = - any isFunctionCall $ analyse findCalls (rootNode params) + -- Check if a function is referenced in command substitution or prompt variables + isFunctionReferencedInCommandSubstitution :: String -> Bool + isFunctionReferencedInCommandSubstitution name = + not . null $ analyse findReferences (rootNode params) where - findCalls token = when (isFunctionCall token) $ modify (token:) - isFunctionCall token = + findReferences token = + case token of + -- Check in $() command substitutions + T_DollarExpansion _ _ -> + when (hasFunctionCall token) $ modify (token:) + -- Check in backtick command substitutions + T_Backticked _ _ -> + when (hasFunctionCall token) $ modify (token:) + -- Check in assignments to prompt variables (PS1, PS2, PS3, PS4, PROMPT_COMMAND) + -- These variables are evaluated by bash even in single quotes + T_Assignment _ _ varname _ value -> + when (varname `elem` promptVars && hasFunctionReference value) $ modify (token:) + _ -> return () + promptVars = ["PS1", "PS2", "PS3", "PS4", "PROMPT_COMMAND"] + hasFunctionCall token = + not . null $ analyse findFunctionCalls token + hasFunctionReference token = + -- Check if function name appears in the value + -- For prompt variables, they can contain: + -- 1. Direct command names (PROMPT_COMMAND='funcname') + -- 2. Command substitutions with $(...) or `...` + case getLiteralString token of + Just str -> + name == str -- Direct function name + || ("$(" ++ name) `isInfixOf` str -- $(funcname) + || ("`" ++ name) `isInfixOf` str -- `funcname` + Nothing -> hasFunctionCall token + findFunctionCalls token = case token of T_SimpleCommand _ _ (cmd:_) -> - getUnquotedLiteral cmd == Just name && not (isUnreachable token) - _ -> False + when (getUnquotedLiteral cmd == Just name) $ modify (token:) + _ -> return () prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]"