Skip to content

Commit 5fc9fbe

Browse files
Copilotdotysan
andcommitted
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>
1 parent 9d02364 commit 5fc9fbe

1 file changed

Lines changed: 37 additions & 11 deletions

File tree

src/ShellCheck/Analytics.hs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5053,8 +5053,8 @@ prop_checkCommandIsUnreachable6 = verifyNot checkCommandIsUnreachable "return ||
50535053
prop_checkCommandIsUnreachable7 = verifyNot checkCommandIsUnreachable "return 2>/dev/null ||:"
50545054
prop_checkCommandIsUnreachable8 = verifyNot checkCommandIsUnreachable "return; echo 'reachable when not in function'"
50555055
prop_checkCommandIsUnreachable9 = verify checkCommandIsUnreachable "f() { return; echo unreachable; }"
5056-
prop_checkCommandIsUnreachable10 = verifyNot checkCommandIsUnreachable "f; f() { :; }; exit $?"
5057-
prop_checkCommandIsUnreachable11 = verifyNot checkCommandIsUnreachable "PS4func; PS4func() { echo test; }; exit $?"
5056+
prop_checkCommandIsUnreachable10 = verifyNot checkCommandIsUnreachable "PS4func() { :; }; PS4='$(PS4func)'; exit"
5057+
prop_checkCommandIsUnreachable11 = verifyNot checkCommandIsUnreachable "f() { :; }; var=`f`; exit"
50585058
checkCommandIsUnreachable params t =
50595059
case t of
50605060
T_Pipeline {} -> sequence_ $ do
@@ -5068,7 +5068,7 @@ checkCommandIsUnreachable params t =
50685068
when (isUnreachableFunction t
50695069
&& (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t)
50705070
&& (not $ isSourced params t)
5071-
&& (not $ isFunctionInvokedInReachableCode name)) $
5071+
&& (not $ isFunctionReferencedInCommandSubstitution name)) $
50725072
info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)."
50735073
_ -> return ()
50745074
where
@@ -5082,17 +5082,43 @@ checkCommandIsUnreachable params t =
50825082
state <- CF.getIncomingState cfga (getId t)
50835083
return . not $ CF.stateIsReachable state
50845084

5085-
-- Check if a function is invoked anywhere in reachable code
5086-
isFunctionInvokedInReachableCode :: String -> Bool
5087-
isFunctionInvokedInReachableCode name =
5088-
any isFunctionCall $ analyse findCalls (rootNode params)
5085+
-- Check if a function is referenced in command substitution or prompt variables
5086+
isFunctionReferencedInCommandSubstitution :: String -> Bool
5087+
isFunctionReferencedInCommandSubstitution name =
5088+
not . null $ analyse findReferences (rootNode params)
50895089
where
5090-
findCalls token = when (isFunctionCall token) $ modify (token:)
5091-
isFunctionCall token =
5090+
findReferences token =
5091+
case token of
5092+
-- Check in $() command substitutions
5093+
T_DollarExpansion _ _ ->
5094+
when (hasFunctionCall token) $ modify (token:)
5095+
-- Check in backtick command substitutions
5096+
T_Backticked _ _ ->
5097+
when (hasFunctionCall token) $ modify (token:)
5098+
-- Check in assignments to prompt variables (PS1, PS2, PS3, PS4, PROMPT_COMMAND)
5099+
-- These variables are evaluated by bash even in single quotes
5100+
T_Assignment _ _ varname _ value ->
5101+
when (varname `elem` promptVars && hasFunctionReference value) $ modify (token:)
5102+
_ -> return ()
5103+
promptVars = ["PS1", "PS2", "PS3", "PS4", "PROMPT_COMMAND"]
5104+
hasFunctionCall token =
5105+
not . null $ analyse findFunctionCalls token
5106+
hasFunctionReference token =
5107+
-- Check if function name appears in the value
5108+
-- For prompt variables, they can contain:
5109+
-- 1. Direct command names (PROMPT_COMMAND='funcname')
5110+
-- 2. Command substitutions with $(...) or `...`
5111+
case getLiteralString token of
5112+
Just str ->
5113+
name == str -- Direct function name
5114+
|| ("$(" ++ name) `isInfixOf` str -- $(funcname)
5115+
|| ("`" ++ name) `isInfixOf` str -- `funcname`
5116+
Nothing -> hasFunctionCall token
5117+
findFunctionCalls token =
50925118
case token of
50935119
T_SimpleCommand _ _ (cmd:_) ->
5094-
getUnquotedLiteral cmd == Just name && not (isUnreachable token)
5095-
_ -> False
5120+
when (getUnquotedLiteral cmd == Just name) $ modify (token:)
5121+
_ -> return ()
50965122

50975123

50985124
prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]"

0 commit comments

Comments
 (0)