Add warning for vars that are referenced but not assigned.
This commit is contained in:
parent
61b4b65184
commit
63188282e9
|
@ -27,6 +27,7 @@ import Data.Functor
|
||||||
import Data.Function (on)
|
import Data.Function (on)
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
|
import Data.Ord
|
||||||
import Debug.Trace
|
import Debug.Trace
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.Options
|
import ShellCheck.Options
|
||||||
|
@ -58,6 +59,7 @@ treeChecks = [
|
||||||
,checkUnpassedInFunctions
|
,checkUnpassedInFunctions
|
||||||
,checkArrayWithoutIndex
|
,checkArrayWithoutIndex
|
||||||
,checkShebang
|
,checkShebang
|
||||||
|
,checkUnassignedReferences
|
||||||
]
|
]
|
||||||
|
|
||||||
checksFor Sh = [
|
checksFor Sh = [
|
||||||
|
@ -331,6 +333,8 @@ deadSimple (T_SimpleCommand _ vars words) = concatMap deadSimple words
|
||||||
deadSimple (T_Redirecting _ _ foo) = deadSimple foo
|
deadSimple (T_Redirecting _ _ foo) = deadSimple foo
|
||||||
deadSimple (T_DollarSingleQuoted _ s) = [s]
|
deadSimple (T_DollarSingleQuoted _ s) = [s]
|
||||||
deadSimple (T_Annotation _ _ s) = deadSimple s
|
deadSimple (T_Annotation _ _ s) = deadSimple s
|
||||||
|
-- Workaround for let "foo = bar" parsing
|
||||||
|
deadSimple (TA_Sequence _ [TA_Expansion _ v]) = concatMap deadSimple v
|
||||||
deadSimple _ = []
|
deadSimple _ = []
|
||||||
|
|
||||||
-- Turn a SimpleCommand foo -avz --bar=baz into args ["a", "v", "z", "bar"]
|
-- Turn a SimpleCommand foo -avz --bar=baz into args ["a", "v", "z", "bar"]
|
||||||
|
@ -367,6 +371,29 @@ checkTree f s = case parseShell "-" s of
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
|
-- Copied from https://wiki.haskell.org/Edit_distance
|
||||||
|
dist :: Eq a => [a] -> [a] -> Int
|
||||||
|
dist a b
|
||||||
|
= last (if lab == 0 then mainDiag
|
||||||
|
else if lab > 0 then lowers !! (lab - 1)
|
||||||
|
else{- < 0 -} uppers !! (-1 - lab))
|
||||||
|
where mainDiag = oneDiag a b (head uppers) (-1 : head lowers)
|
||||||
|
uppers = eachDiag a b (mainDiag : uppers) -- upper diagonals
|
||||||
|
lowers = eachDiag b a (mainDiag : lowers) -- lower diagonals
|
||||||
|
eachDiag a [] diags = []
|
||||||
|
eachDiag a (bch:bs) (lastDiag:diags) = oneDiag a bs nextDiag lastDiag : eachDiag a bs diags
|
||||||
|
where nextDiag = head (tail diags)
|
||||||
|
oneDiag a b diagAbove diagBelow = thisdiag
|
||||||
|
where doDiag [] b nw n w = []
|
||||||
|
doDiag a [] nw n w = []
|
||||||
|
doDiag (ach:as) (bch:bs) nw n w = me : (doDiag as bs me (tail n) (tail w))
|
||||||
|
where me = if ach == bch then nw else 1 + min3 (head w) nw (head n)
|
||||||
|
firstelt = 1 + head diagBelow
|
||||||
|
thisdiag = firstelt : doDiag a b firstelt diagAbove (tail diagBelow)
|
||||||
|
lab = length a - length b
|
||||||
|
min3 x y z = if x < y then x else min y z
|
||||||
|
|
||||||
|
|
||||||
prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)"
|
prop_checkEchoWc3 = verify checkEchoWc "n=$(echo $foo | wc -c)"
|
||||||
checkEchoWc _ (T_Pipeline id _ [a, b]) =
|
checkEchoWc _ (T_Pipeline id _ [a, b]) =
|
||||||
when (acmd == ["echo", "${VAR}"]) $
|
when (acmd == ["echo", "${VAR}"]) $
|
||||||
|
@ -2093,6 +2120,11 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||||
"read" ->
|
"read" ->
|
||||||
let params = map getLiteral rest in
|
let params = map getLiteral rest in
|
||||||
catMaybes . takeWhile isJust . reverse $ params
|
catMaybes . takeWhile isJust . reverse $ params
|
||||||
|
"getopts" ->
|
||||||
|
case rest of
|
||||||
|
opts:var:_ -> maybeToList $ getLiteral var
|
||||||
|
_ -> []
|
||||||
|
|
||||||
"let" -> concatMap letParamToLiteral rest
|
"let" -> concatMap letParamToLiteral rest
|
||||||
|
|
||||||
"export" -> concatMap getModifierParam rest
|
"export" -> concatMap getModifierParam rest
|
||||||
|
@ -2139,17 +2171,16 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
|
||||||
|
|
||||||
getModifiedVariableCommand _ = []
|
getModifiedVariableCommand _ = []
|
||||||
|
|
||||||
-- TODO:
|
|
||||||
getBracedReference s =
|
getBracedReference s =
|
||||||
case filter (not . null) [
|
let name = takeName $ dropPrefix s in
|
||||||
dropSuffix $ dropPrefix s,
|
if null name then s else name
|
||||||
dropSuffix s,
|
|
||||||
s] of
|
|
||||||
(a:_) -> a
|
|
||||||
[] -> error "Internal ShellCheck error (empty braced reference). Please file a bug!"
|
|
||||||
where
|
where
|
||||||
dropSuffix = takeWhile (`notElem` ":[#%/^,")
|
|
||||||
dropPrefix = dropWhile (`elem` "#!")
|
dropPrefix = dropWhile (`elem` "#!")
|
||||||
|
takeName s =
|
||||||
|
let special = getSpecial s in
|
||||||
|
if null special then takeWhile isVariableChar s else special
|
||||||
|
getSpecial (c:_) =
|
||||||
|
if c `elem` "*@#?-$!" then [c] else ""
|
||||||
|
|
||||||
getIndexReferences s = fromMaybe [] $ do
|
getIndexReferences s = fromMaybe [] $ do
|
||||||
(_, index, _, _) <- matchRegexAll re s
|
(_, index, _, _) <- matchRegexAll re s
|
||||||
|
@ -2491,12 +2522,84 @@ checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)
|
||||||
unused = Map.assocs $ Map.difference assignments references
|
unused = Map.assocs $ Map.difference assignments references
|
||||||
|
|
||||||
warnFor (name, token) =
|
warnFor (name, token) =
|
||||||
info (getId token) 2034 $
|
warn (getId token) 2034 $
|
||||||
name ++ " appears unused. Verify it or export it."
|
name ++ " appears unused. Verify it or export it."
|
||||||
|
|
||||||
stripSuffix = takeWhile isVariableChar
|
stripSuffix = takeWhile isVariableChar
|
||||||
defaultMap = Map.fromList $ zip internalVariables $ repeat ()
|
defaultMap = Map.fromList $ zip internalVariables $ repeat ()
|
||||||
|
|
||||||
|
prop_checkUnassignedReferences1 = verifyTree checkUnassignedReferences "echo $foo"
|
||||||
|
prop_checkUnassignedReferences2 = verifyNotTree checkUnassignedReferences "foo=hello; echo $foo"
|
||||||
|
prop_checkUnassignedReferences3 = verifyTree checkUnassignedReferences "MY_VALUE=3; echo $MYVALUE"
|
||||||
|
prop_checkUnassignedReferences4 = verifyNotTree checkUnassignedReferences "RANDOM2=foo; echo $RANDOM"
|
||||||
|
prop_checkUnassignedReferences5 = verifyNotTree checkUnassignedReferences "declare -A foo=([bar]=baz); echo ${foo[bar]}"
|
||||||
|
prop_checkUnassignedReferences6 = verifyNotTree checkUnassignedReferences "foo=..; echo ${foo-bar}"
|
||||||
|
prop_checkUnassignedReferences7 = verifyNotTree checkUnassignedReferences "getopts ':h' foo; echo $foo"
|
||||||
|
prop_checkUnassignedReferences8 = verifyNotTree checkUnassignedReferences "let 'foo = 1'; echo $foo"
|
||||||
|
prop_checkUnassignedReferences9 = verifyNotTree checkUnassignedReferences "echo ${foo-bar}"
|
||||||
|
prop_checkUnassignedReferences10= verifyNotTree checkUnassignedReferences "echo ${foo:?}"
|
||||||
|
checkUnassignedReferences params t = warnings
|
||||||
|
where
|
||||||
|
(readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty)
|
||||||
|
defaultAssigned = Map.fromList $ map (\a -> (a, ())) $ filter (not . null) internalVariables
|
||||||
|
|
||||||
|
tally (Assignment (_, _, name, _)) =
|
||||||
|
modify (\(read, written) -> (read, Map.insert name () written))
|
||||||
|
tally (Reference (_, place, name)) =
|
||||||
|
modify (\(read, written) -> (Map.insertWith' (const id) name place read, written))
|
||||||
|
tally _ = return ()
|
||||||
|
|
||||||
|
unassigned = Map.toList $ Map.difference (Map.difference readMap writeMap) defaultAssigned
|
||||||
|
writtenVars = filter isVariableName $ Map.keys writeMap
|
||||||
|
|
||||||
|
getBestMatch var = do
|
||||||
|
(match, score) <- listToMaybe best
|
||||||
|
guard $ goodMatch var match score
|
||||||
|
return match
|
||||||
|
where
|
||||||
|
matches = map (\x -> (x, dist var x)) writtenVars
|
||||||
|
best = sortBy (comparing snd) matches
|
||||||
|
goodMatch var match score =
|
||||||
|
let l = length match in
|
||||||
|
l > 3 && score <= 1
|
||||||
|
|| l > 7 && score <= 2
|
||||||
|
|
||||||
|
isLocal = any isLower
|
||||||
|
|
||||||
|
warningForGlobals var place = do
|
||||||
|
match <- getBestMatch var
|
||||||
|
return $ warn (getId place) 2153 $
|
||||||
|
"Possible misspelling: " ++ var ++ " may not be assigned, but " ++ match ++ " is."
|
||||||
|
|
||||||
|
warningForLocals var place =
|
||||||
|
return $ warn (getId place) 2154 $
|
||||||
|
var ++ " is referenced but apparently never assigned" ++ optionalTip ++ "."
|
||||||
|
where
|
||||||
|
optionalTip = fromMaybe "" $ do
|
||||||
|
match <- getBestMatch var
|
||||||
|
return $ " (did you mean '" ++ match ++ "'?)"
|
||||||
|
|
||||||
|
warningFor var place = do
|
||||||
|
guard . not $ isInArray place || isGuarded place
|
||||||
|
(if isLocal var then warningForLocals else warningForGlobals) var place
|
||||||
|
|
||||||
|
warnings = execWriter . sequence $ mapMaybe (uncurry warningFor) unassigned
|
||||||
|
|
||||||
|
-- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays.
|
||||||
|
-- This works around it by ignoring references in array assignemnts.
|
||||||
|
isInArray t = any isArray $ getPath (parentMap params) t
|
||||||
|
where
|
||||||
|
isArray (T_Array {}) = True
|
||||||
|
isArray _ = False
|
||||||
|
|
||||||
|
isGuarded (T_DollarBraced _ v) =
|
||||||
|
any (`isPrefixOf` rest) ["-", ":-", "?", ":?"]
|
||||||
|
where
|
||||||
|
name = concat $ deadSimple v
|
||||||
|
rest = dropWhile isVariableChar $ dropWhile (`elem` "#!") $ name
|
||||||
|
isGuarded _ = False
|
||||||
|
|
||||||
|
|
||||||
prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"
|
prop_checkGlobsAsOptions1 = verify checkGlobsAsOptions "rm *.txt"
|
||||||
prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*"
|
prop_checkGlobsAsOptions2 = verify checkGlobsAsOptions "ls ??.*"
|
||||||
prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"
|
prop_checkGlobsAsOptions3 = verifyNot checkGlobsAsOptions "rm -- *.txt"
|
||||||
|
@ -3091,5 +3194,5 @@ checkFindExecWithSingleArgument _ = checkCommand "find" (const f)
|
||||||
check _ = Nothing
|
check _ = Nothing
|
||||||
commandRegex = mkRegex "[ |;]"
|
commandRegex = mkRegex "[ |;]"
|
||||||
|
|
||||||
return []
|
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
||||||
|
|
Loading…
Reference in New Issue