hadolint-2.14.0: src/Hadolint/Rule/DL3044.hs
module Hadolint.Rule.DL3044 (rule) where
import qualified Data.Set as Set
import qualified Data.Text as Text
import Hadolint.Rule
import Language.Docker.Syntax
rule :: Rule args
rule = customRule check (emptyState Set.empty)
where
code = "DL3044"
severity = DLErrorC
message = "Do not refer to an environment variable within the same `ENV` statement where it is defined."
check line st (Env pairs) =
let newState = st |> modify (Set.union (Set.fromList (map fst pairs)))
in if null [env | env <- listOfReferences pairs, env `Set.notMember` state st]
then newState
else newState |> addFail CheckFailure {..}
check _ st (Arg arg _) = st |> modify (Set.insert arg)
check _ st _ = st
{-# INLINEABLE rule #-}
-- | generates a list of references to variable names referenced on the right
-- hand side of a variable definition, except when the variable is
-- referenced on its own right hand side.
listOfReferences :: Pairs -> [Text.Text]
listOfReferences prs =
[ var
| (idx, (var, _)) <- enum prs,
var `isSubstringOfAny` map (snd . snd) (filter ((/= idx) . fst) (enum prs))
]
where
enum :: Pairs -> [(Integer, (Text.Text, Text.Text))]
enum = zip [0..]
-- | is a reference of a variable substring of any text?
-- matches ${var_name} and $var_name, but not $var_nameblafoo
isSubstringOfAny :: Text.Text -> [Text.Text] -> Bool
isSubstringOfAny t l =
not $
null
[ v
| v <- l,
(Text.pack "${" <> t <> Text.pack "}") `Text.isInfixOf` v
|| (t `bareVariableInText` v)
]
-- | we find a 'bare' variable with name v in a text, if
-- '$v' is in the text at any place and any text following after that
-- occurence would terminate a variable name. To determine that, the text t
-- is split at every occurence of var, check if '$v' is in the text and if
-- any part of the split text would terminate a variable name.
bareVariableInText :: Text.Text -> Text.Text -> Bool
bareVariableInText v t =
let var = "$" <> v
rest = drop 1 $ Text.splitOn var t
in var `Text.isInfixOf` t && any terminatesVarName rest
where
-- x would terminate a variable name if it was appended directly to
-- that name
terminatesVarName :: Text.Text -> Bool
terminatesVarName x = Text.null x || not (beginsWithAnyOf x varChar)
-- txt begins with any character of String
beginsWithAnyOf :: Text.Text -> Set.Set Char -> Bool
beginsWithAnyOf txt str = any (`Text.isPrefixOf` txt) (Set.map Text.singleton str)
-- all characters valid in the inner of a shell variable name
varChar :: Set.Set Char
varChar = Set.fromList (['0' .. '9'] ++ ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['_'])