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 :: Rule args
rule = (Linenumber
 -> State (Set Text) -> Instruction args -> State (Set Text))
-> State (Set Text) -> Rule args
forall a args.
(Linenumber -> State a -> Instruction args -> State a)
-> State a -> Rule args
customRule Linenumber
-> State (Set Text) -> Instruction args -> State (Set Text)
forall args.
Linenumber
-> State (Set Text) -> Instruction args -> State (Set Text)
check (Set Text -> State (Set Text)
forall a. a -> State a
emptyState Set Text
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 :: Linenumber
-> State (Set Text) -> Instruction args -> State (Set Text)
check Linenumber
line State (Set Text)
st (Env Pairs
pairs) =
      let newState :: State (Set Text)
newState = State (Set Text)
st State (Set Text)
-> (State (Set Text) -> State (Set Text)) -> State (Set Text)
forall a b. a -> (a -> b) -> b
|> (Set Text -> Set Text) -> State (Set Text) -> State (Set Text)
forall a. (a -> a) -> State a -> State a
modify (Set Text -> Set Text -> Set Text
forall a. Ord a => Set a -> Set a -> Set a
Set.union ([Text] -> Set Text
forall a. Ord a => [a] -> Set a
Set.fromList (((Text, Text) -> Text) -> Pairs -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text, Text) -> Text
forall a b. (a, b) -> a
fst Pairs
pairs)))
       in if [Text] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Text
env | Text
env <- Pairs -> [Text]
listOfReferences Pairs
pairs, Text
env Text -> Set Text -> Bool
forall a. Ord a => a -> Set a -> Bool
`Set.notMember` State (Set Text) -> Set Text
forall a. State a -> a
state State (Set Text)
st]
            then State (Set Text)
newState
            else State (Set Text)
newState State (Set Text)
-> (State (Set Text) -> State (Set Text)) -> State (Set Text)
forall a b. a -> (a -> b) -> b
|> CheckFailure -> State (Set Text) -> State (Set Text)
forall a. CheckFailure -> State a -> State a
addFail CheckFailure :: RuleCode -> DLSeverity -> Text -> Linenumber -> CheckFailure
CheckFailure {Linenumber
Text
RuleCode
DLSeverity
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
line :: Linenumber
message :: Text
severity :: DLSeverity
code :: RuleCode
..}
    check Linenumber
_ State (Set Text)
st (Arg Text
arg Maybe Text
_) = State (Set Text)
st State (Set Text)
-> (State (Set Text) -> State (Set Text)) -> State (Set Text)
forall a b. a -> (a -> b) -> b
|> (Set Text -> Set Text) -> State (Set Text) -> State (Set Text)
forall a. (a -> a) -> State a -> State a
modify (Text -> Set Text -> Set Text
forall a. Ord a => a -> Set a -> Set a
Set.insert Text
arg)
    check Linenumber
_ 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
    | (Linenumber
idx, (Text
var, Text
_)) <- Pairs -> [(Linenumber, (Text, Text))]
forall a. [a] -> [(Linenumber, a)]
indexed Pairs
prs,
      Text
var Text -> [Text] -> Bool
`isSubstringOfAny` ((Linenumber, (Text, Text)) -> Text)
-> [(Linenumber, (Text, Text))] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map ((Text, Text) -> Text
forall a b. (a, b) -> b
snd ((Text, Text) -> Text)
-> ((Linenumber, (Text, Text)) -> (Text, Text))
-> (Linenumber, (Text, Text))
-> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Linenumber, (Text, Text)) -> (Text, Text)
forall a b. (a, b) -> b
snd) (((Linenumber, (Text, Text)) -> Bool)
-> [(Linenumber, (Text, Text))] -> [(Linenumber, (Text, Text))]
forall a. (a -> Bool) -> [a] -> [a]
filter ((Linenumber -> Linenumber -> Bool
forall a. Eq a => a -> a -> Bool
/= Linenumber
idx) (Linenumber -> Bool)
-> ((Linenumber, (Text, Text)) -> Linenumber)
-> (Linenumber, (Text, Text))
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Linenumber, (Text, Text)) -> Linenumber
forall a b. (a, b) -> a
fst) (Pairs -> [(Linenumber, (Text, Text))]
forall a. [a] -> [(Linenumber, 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 (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$
    [Text] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null
      [ Text
v
        | Text
v <- [Text]
l,
          (String -> Text
Text.pack String
"${" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
t Text -> Text -> Text
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
"$" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
v
      rest :: [Text]
rest = Linenumber -> [Text] -> [Text]
forall a. Linenumber -> [a] -> [a]
drop Linenumber
1 ([Text] -> [Text]) -> [Text] -> [Text]
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
&& (Text -> Bool) -> [Text] -> 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 = (Text -> Bool) -> Set Text -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> Text -> Bool
`Text.isPrefixOf` Text
txt) ((Char -> Text) -> Set Char -> Set Text
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 = String -> Set Char
forall a. Ord a => [a] -> Set a
Set.fromList ([Char
'0' .. Char
'9'] String -> String -> String
forall a. [a] -> [a] -> [a]
++ [Char
'a' .. Char
'z'] String -> String -> String
forall a. [a] -> [a] -> [a]
++ [Char
'A' .. Char
'Z'] String -> String -> String
forall a. [a] -> [a] -> [a]
++ [Char
'_'])