module Hadolint.Rule.DL3044 (rule) where

import Data.List.Index (indexed)
import qualified Data.Set as Set
import qualified Data.Text as Text
import Hadolint.Rule
import Language.Docker.Syntax

rule :: Rule args
rule :: forall args. Rule args
rule = forall a args.
(Int -> State a -> Instruction args -> State a)
-> State a -> Rule args
customRule forall {args}.
Int -> State (Set Text) -> Instruction args -> State (Set Text)
check (forall a. a -> State a
emptyState forall a. Set a
Set.empty)
  where
    code :: RuleCode
code = RuleCode
"DL3044"
    severity :: DLSeverity
severity = DLSeverity
DLErrorC
    message :: Text
message = Text
"Do not refer to an environment variable within the same `ENV` statement where it is defined."

    check :: Int -> State (Set Text) -> Instruction args -> State (Set Text)
check Int
line State (Set Text)
st (Env Pairs
pairs) =
      let newState :: State (Set Text)
newState = State (Set Text)
st forall a b. a -> (a -> b) -> b
|> forall a. (a -> a) -> State a -> State a
modify (forall a. Ord a => Set a -> Set a -> Set a
Set.union (forall a. Ord a => [a] -> Set a
Set.fromList (forall a b. (a -> b) -> [a] -> [b]
map forall a b. (a, b) -> a
fst Pairs
pairs)))
       in if forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Text
env | Text
env <- Pairs -> [Text]
listOfReferences Pairs
pairs, Text
env forall a. Ord a => a -> Set a -> Bool
`Set.notMember` forall a. State a -> a
state State (Set Text)
st]
            then State (Set Text)
newState
            else State (Set Text)
newState forall a b. a -> (a -> b) -> b
|> forall a. CheckFailure -> State a -> State a
addFail CheckFailure {Int
Text
RuleCode
DLSeverity
line :: Int
message :: Text
severity :: DLSeverity
code :: RuleCode
line :: Int
message :: Text
severity :: DLSeverity
code :: RuleCode
..}
    check Int
_ State (Set Text)
st (Arg Text
arg Maybe Text
_) = State (Set Text)
st forall a b. a -> (a -> b) -> b
|> forall a. (a -> a) -> State a -> State a
modify (forall a. Ord a => a -> Set a -> Set a
Set.insert Text
arg)
    check Int
_ State (Set Text)
st Instruction args
_ = State (Set Text)
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 :: Pairs -> [Text]
listOfReferences Pairs
prs =
  [ Text
var
    | (Int
idx, (Text
var, Text
_)) <- forall a. [a] -> [(Int, a)]
indexed Pairs
prs,
      Text
var Text -> [Text] -> Bool
`isSubstringOfAny` forall a b. (a -> b) -> [a] -> [b]
map (forall a b. (a, b) -> b
snd forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a, b) -> b
snd) (forall a. (a -> Bool) -> [a] -> [a]
filter ((forall a. Eq a => a -> a -> Bool
/= Int
idx) forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a, b) -> a
fst) (forall a. [a] -> [(Int, a)]
indexed Pairs
prs))
  ]

-- | 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 :: Text -> [Text] -> Bool
isSubstringOfAny Text
t [Text]
l =
  Bool -> Bool
not forall a b. (a -> b) -> a -> b
$
    forall (t :: * -> *) a. Foldable t => t a -> Bool
null
      [ Text
v
        | Text
v <- [Text]
l,
          (String -> Text
Text.pack String
"${" forall a. Semigroup a => a -> a -> a
<> Text
t forall a. Semigroup a => a -> a -> a
<> String -> Text
Text.pack String
"}") Text -> Text -> Bool
`Text.isInfixOf` Text
v
            Bool -> Bool -> Bool
|| (Text
t Text -> Text -> Bool
`bareVariableInText` Text
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 :: Text -> Text -> Bool
bareVariableInText Text
v Text
t =
  let var :: Text
var = Text
"$" forall a. Semigroup a => a -> a -> a
<> Text
v
      rest :: [Text]
rest = forall a. Int -> [a] -> [a]
drop Int
1 forall a b. (a -> b) -> a -> b
$ Text -> Text -> [Text]
Text.splitOn Text
var Text
t
   in Text
var Text -> Text -> Bool
`Text.isInfixOf` Text
t Bool -> Bool -> Bool
&& forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any Text -> Bool
terminatesVarName [Text]
rest
  where
    -- x would terminate a variable name if it was appended directly to
    -- that name
    terminatesVarName :: Text.Text -> Bool
    terminatesVarName :: Text -> Bool
terminatesVarName Text
x = Text -> Bool
Text.null Text
x Bool -> Bool -> Bool
|| Bool -> Bool
not (Text -> Set Char -> Bool
beginsWithAnyOf Text
x Set Char
varChar)

    -- txt begins with any character of String
    beginsWithAnyOf :: Text.Text -> Set.Set Char -> Bool
    beginsWithAnyOf :: Text -> Set Char -> Bool
beginsWithAnyOf Text
txt Set Char
str = forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> Text -> Bool
`Text.isPrefixOf` Text
txt) (forall b a. Ord b => (a -> b) -> Set a -> Set b
Set.map Char -> Text
Text.singleton Set Char
str)

    -- all characters valid in the inner of a shell variable name
    varChar :: Set.Set Char
    varChar :: Set Char
varChar = forall a. Ord a => [a] -> Set a
Set.fromList ([Char
'0' .. Char
'9'] forall a. [a] -> [a] -> [a]
++ [Char
'a' .. Char
'z'] forall a. [a] -> [a] -> [a]
++ [Char
'A' .. Char
'Z'] forall a. [a] -> [a] -> [a]
++ [Char
'_'])