packages feed

hadolint 1.6.6 → 1.7.0

raw patch · 9 files changed

+232/−191 lines, 9 filesdep +megaparsecdep +voiddep −parsecdep ~ShellCheckdep ~language-dockerPVP ok

version bump matches the API change (PVP)

Dependencies added: megaparsec, void

Dependencies removed: parsec

Dependency ranges changed: ShellCheck, language-docker

API changes (from Hackage documentation)

- Hadolint.Formatter.Format: formatErrorReason :: ParseError -> String
- Hadolint.Formatter.Format: instance Data.Semigroup.Semigroup Hadolint.Formatter.Format.Result
- Hadolint.Formatter.Format: instance GHC.Base.Monoid Hadolint.Formatter.Format.Result
- Hadolint.Formatter.Format: instance GHC.Classes.Eq Hadolint.Formatter.Format.Result
- Hadolint.Formatter.Json: instance Data.Aeson.Types.Class.ToJSON Hadolint.Formatter.Json.JsonFormat
+ Hadolint.Formatter.Format: instance (GHC.Classes.Eq t, GHC.Classes.Eq e) => GHC.Classes.Eq (Hadolint.Formatter.Format.Result t e)
+ Hadolint.Formatter.Format: instance Data.Semigroup.Semigroup (Hadolint.Formatter.Format.Result t e)
+ Hadolint.Formatter.Format: instance GHC.Base.Monoid (Hadolint.Formatter.Format.Result t e)
+ Hadolint.Formatter.Json: instance (Text.Megaparsec.Error.ShowToken t, GHC.Classes.Ord t, Text.Megaparsec.Error.ShowErrorComponent e) => Data.Aeson.Types.Class.ToJSON (Hadolint.Formatter.Json.JsonFormat t e)
+ Hadolint.Rules: argumentsRule :: ([String] -> a) -> Arguments Text -> a
+ Hadolint.Rules: type IgnoreRuleParser = Parsec Void Text
- Hadolint.Formatter.Checkstyle: formatResult :: Result -> Builder
+ Hadolint.Formatter.Checkstyle: formatResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> Builder
- Hadolint.Formatter.Checkstyle: printResult :: Result -> IO ()
+ Hadolint.Formatter.Checkstyle: printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO ()
- Hadolint.Formatter.Codeclimate: formatResult :: Result -> Seq Issue
+ Hadolint.Formatter.Codeclimate: formatResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> Seq Issue
- Hadolint.Formatter.Codeclimate: printResult :: Result -> IO ()
+ Hadolint.Formatter.Codeclimate: printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO ()
- Hadolint.Formatter.Format: Result :: Seq ParseError -> Seq RuleCheck -> Result
+ Hadolint.Formatter.Format: Result :: Seq (ParseError t e) -> Seq RuleCheck -> Result t e
- Hadolint.Formatter.Format: [checks] :: Result -> Seq RuleCheck
+ Hadolint.Formatter.Format: [checks] :: Result t e -> Seq RuleCheck
- Hadolint.Formatter.Format: [errors] :: Result -> Seq ParseError
+ Hadolint.Formatter.Format: [errors] :: Result t e -> Seq (ParseError t e)
- Hadolint.Formatter.Format: data Result
+ Hadolint.Formatter.Format: data Result t e
- Hadolint.Formatter.Format: toResult :: Either ParseError [RuleCheck] -> Result
+ Hadolint.Formatter.Format: toResult :: Either (ParseError t e) [RuleCheck] -> Result t e
- Hadolint.Formatter.Json: formatResult :: Result -> Value
+ Hadolint.Formatter.Json: formatResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> Value
- Hadolint.Formatter.Json: printResult :: Result -> IO ()
+ Hadolint.Formatter.Json: printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO ()
- Hadolint.Formatter.TTY: formatError :: ParseError -> String
+ Hadolint.Formatter.TTY: formatError :: (ShowToken t, Ord t, ShowErrorComponent e) => ParseError t e -> String
- Hadolint.Formatter.TTY: printResult :: Result -> IO ()
+ Hadolint.Formatter.TTY: printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO ()
- Hadolint.Rules: Metadata :: String -> Severity -> String -> Metadata
+ Hadolint.Rules: Metadata :: Text -> Severity -> Text -> Metadata
- Hadolint.Rules: [code] :: Metadata -> String
+ Hadolint.Rules: [code] :: Metadata -> Text
- Hadolint.Rules: [message] :: Metadata -> String
+ Hadolint.Rules: [message] :: Metadata -> Text
- Hadolint.Rules: aliasMustBe :: (String -> Bool) -> Instruction -> Bool
+ Hadolint.Rules: aliasMustBe :: (Text -> Bool) -> Instruction a -> Bool
- Hadolint.Rules: allImageNames :: Dockerfile -> [(Linenumber, String)]
+ Hadolint.Rules: allImageNames :: Dockerfile -> [(Linenumber, Text)]
- Hadolint.Rules: fromName :: BaseImage -> String
+ Hadolint.Rules: fromName :: BaseImage -> Text
- Hadolint.Rules: ignored :: Dockerfile -> [(Linenumber, [String])]
+ Hadolint.Rules: ignored :: Dockerfile -> [(Linenumber, [Text])]
- Hadolint.Rules: instructionRule :: String -> Severity -> String -> (Instruction -> Bool) -> Rule
+ Hadolint.Rules: instructionRule :: Text -> Severity -> Text -> (Instruction Text -> Bool) -> Rule
- Hadolint.Rules: instructionRuleLine :: String -> Severity -> String -> (Linenumber -> Instruction -> Bool) -> Rule
+ Hadolint.Rules: instructionRuleLine :: Text -> Severity -> Text -> (Linenumber -> Instruction Text -> Bool) -> Rule
- Hadolint.Rules: instructionRuleState :: String -> Severity -> String -> (state -> Linenumber -> Instruction -> (state, Bool)) -> state -> Rule
+ Hadolint.Rules: instructionRuleState :: Text -> Severity -> Text -> (state -> Linenumber -> Instruction Text -> (state, Bool)) -> state -> Rule
- Hadolint.Rules: isArchive :: String -> Bool
+ Hadolint.Rules: isArchive :: Text -> Bool
- Hadolint.Rules: isUrl :: String -> Bool
+ Hadolint.Rules: isUrl :: Text -> Bool
- Hadolint.Rules: link :: Metadata -> String
+ Hadolint.Rules: link :: Metadata -> Text
- Hadolint.Rules: mapInstructions :: Metadata -> (state -> Linenumber -> Instruction -> (state, Bool)) -> state -> Rule
+ Hadolint.Rules: mapInstructions :: Metadata -> (state -> Linenumber -> Instruction Text -> (state, Bool)) -> state -> Rule
- Hadolint.Rules: previouslyDefinedAliases :: Linenumber -> Dockerfile -> [String]
+ Hadolint.Rules: previouslyDefinedAliases :: Linenumber -> Dockerfile -> [Text]

Files

app/Main.hs view
@@ -5,8 +5,8 @@ module Main where  import Hadolint.Rules-import Language.Docker.Parser-import Language.Docker.Syntax+import Language.Docker (parseFile)+import Language.Docker.Syntax (Dockerfile)  import Control.Applicative import Control.Monad (filterM)@@ -23,15 +23,15 @@         getXdgDirectory) import System.Exit (exitFailure, exitSuccess) import System.FilePath ((</>))-import Text.Parsec (ParseError) +import Data.Text (Text) import qualified Hadolint.Formatter.Checkstyle as Checkstyle import qualified Hadolint.Formatter.Codeclimate as Codeclimate import Hadolint.Formatter.Format (toResult) import qualified Hadolint.Formatter.Json as Json import qualified Hadolint.Formatter.TTY as TTY -type IgnoreRule = String+type IgnoreRule = Text  data OutputFormat     = Json@@ -144,15 +144,6 @@ parseFilename "-" = "/dev/stdin" parseFilename s = s -lintDockerfile :: [IgnoreRule] -> String -> IO (Either ParseError [RuleCheck])-lintDockerfile ignoreRules dockerFile = do-    ast <- parseFile $ parseFilename dockerFile-    return (processedFile ast)-  where-    processedFile = fmap processRules-    processRules fileLines = filter ignoredRules (analyzeAll fileLines)-    ignoredRules = ignoreFilter ignoreRules- getVersion :: String getVersion     | $(gitDescribe) == "UNKNOWN" =@@ -179,6 +170,13 @@             Json -> Json.printResult res             Checkstyle -> Checkstyle.printResult res             CodeclimateJson -> Codeclimate.printResult res >> exitSuccess+    lintDockerfile ignoreRules dockerFile = do+        ast <- parseFile $ parseFilename dockerFile+        return (processedFile ast)+      where+        processedFile = fmap processRules+        processRules fileLines = filter ignoredRules (analyzeAll fileLines)+        ignoredRules = ignoreFilter ignoreRules  analyzeAll :: Dockerfile -> [RuleCheck] analyzeAll = analyze rules
hadolint.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 913a53c45d2193a9fb13a8dfb47bf647d985298fc2522ce1e369ab2ed1d1a311+-- hash: f6ff47c57b802a33bbcf1b9944fe11fdf7e5c7913120d5bee6c33d93e69df6e2  name:           hadolint-version:        1.6.6+version:        1.7.0 synopsis:       Dockerfile Linter JavaScript API description:    A smarter Dockerfile linter that helps you build best practice Docker images. category:       Development@@ -29,15 +29,16 @@       src   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints   build-depends:-      ShellCheck >=0.4.7+      ShellCheck >=0.5.0     , aeson     , base >=4.8 && <5     , bytestring     , containers-    , language-docker >=5.0.0 && <6-    , parsec >=3.1+    , language-docker >=6.0.1 && <7+    , megaparsec >=6.4     , split >=0.2     , text+    , void   exposed-modules:       Hadolint.Bash       Hadolint.Rules@@ -61,9 +62,10 @@     , filepath     , gitrev >=1.3.1     , hadolint-    , language-docker >=5.0.0 && <6+    , language-docker >=6.0.1 && <7+    , megaparsec >=6.4     , optparse-applicative-    , parsec >=3.1+    , text     , yaml   if !(os(osx))     ld-options: -static -pthread@@ -79,15 +81,16 @@   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints   build-depends:       HUnit >=1.2-    , ShellCheck >=0.4.7+    , ShellCheck >=0.5.0     , aeson     , base >=4.8 && <5     , bytestring >=0.10     , hadolint     , hspec-    , language-docker >=5.0.0 && <6-    , parsec >=3.1+    , language-docker >=6.0.1 && <7+    , megaparsec >=6.4     , split >=0.2+    , text   other-modules:       Paths_hadolint   default-language: Haskell2010
src/Hadolint/Formatter/Checkstyle.hs view
@@ -11,13 +11,17 @@ import Data.Char import Data.Foldable (toList) import Data.List (groupBy)+import qualified Data.List.NonEmpty as NE import Data.Monoid ((<>), mconcat)+import qualified Data.Text as Text import Hadolint.Formatter.Format import Hadolint.Rules (Metadata(..), RuleCheck(..)) import ShellCheck.Interface-import Text.Parsec (ParseError)-import Text.Parsec.Error (errorPos)-import Text.Parsec.Pos+import Text.Megaparsec.Error+       (ParseError, ShowErrorComponent, ShowToken, errorPos,+        parseErrorTextPretty)+import Text.Megaparsec.Pos+       (sourceColumn, sourceLine, sourceName, unPos)  data CheckStyle = CheckStyle     { file :: String@@ -28,28 +32,28 @@     , source :: String     } -errorToCheckStyle :: ParseError -> CheckStyle+errorToCheckStyle :: (ShowToken t, Ord t, ShowErrorComponent e) => ParseError t e -> CheckStyle errorToCheckStyle err =     CheckStyle     { file = sourceName pos-    , line = sourceLine pos-    , column = sourceColumn pos+    , line = unPos (sourceLine pos)+    , column = unPos (sourceColumn pos)     , impact = severityText ErrorC-    , msg = stripNewlines (formatErrorReason err)+    , msg = stripNewlines (parseErrorTextPretty err)     , source = "DL1000"     }   where-    pos = errorPos err+    pos = NE.head (errorPos err)  ruleToCheckStyle :: RuleCheck -> CheckStyle ruleToCheckStyle RuleCheck {..} =     CheckStyle-    { file = filename+    { file = Text.unpack filename     , line = linenumber     , column = 1     , impact = severityText (severity metadata)-    , msg = message metadata-    , source = code metadata+    , msg = Text.unpack (message metadata)+    , source = Text.unpack (code metadata)     }  toXml :: [CheckStyle] -> Builder.Builder@@ -81,7 +85,7 @@             else "&#" ++ show (ord c) ++ ";"     isOk x = any (\check -> check x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` [' ', '.', '/'])] -formatResult :: Result -> Builder.Builder+formatResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> Builder.Builder formatResult (Result errors checks) =     "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>" <> xmlBody <> "</checkstyle>"   where@@ -92,5 +96,5 @@     checkstyleChecks = fmap ruleToCheckStyle checks     sameFileName CheckStyle {file = f1} CheckStyle {file = f2} = f1 == f2 -printResult :: Result -> IO ()+printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO () printResult result = B.putStr (Builder.toLazyByteString (formatResult result))
src/Hadolint/Formatter/Codeclimate.hs view
@@ -9,14 +9,19 @@  import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy as B+import qualified Data.List.NonEmpty as NE import Data.Monoid ((<>)) import Data.Sequence (Seq)+import qualified Data.Text as Text import GHC.Generics-import Hadolint.Formatter.Format (Result(..), formatErrorReason)+import Hadolint.Formatter.Format (Result(..)) import Hadolint.Rules (Metadata(..), RuleCheck(..)) import ShellCheck.Interface-import Text.Parsec.Error (ParseError, errorPos)-import Text.Parsec.Pos+import Text.Megaparsec.Error+       (ParseError, ShowErrorComponent, ShowToken, errorPos,+        parseErrorTextPretty)+import Text.Megaparsec.Pos+       (sourceColumn, sourceLine, sourceName, unPos)  data Issue = Issue     { checkName :: String@@ -54,25 +59,25 @@             , "severity" .= impact             ] -errorToIssue :: ParseError -> Issue+errorToIssue :: (ShowToken t, Ord t, ShowErrorComponent e) => ParseError t e -> Issue errorToIssue err =     Issue     { checkName = "DL1000"-    , description = formatErrorReason err+    , description = parseErrorTextPretty err     , location = LocPos (sourceName pos) Pos {..}     , impact = severityText ErrorC     }   where-    pos = errorPos err-    line = sourceLine pos-    column = sourceColumn pos+    pos = NE.head (errorPos err)+    line = unPos (sourceLine pos)+    column = unPos (sourceColumn pos)  checkToIssue :: RuleCheck -> Issue checkToIssue RuleCheck {..} =     Issue-    { checkName = code metadata-    , description = message metadata-    , location = LocLine filename linenumber+    { checkName = Text.unpack (code metadata)+    , description = Text.unpack (message metadata)+    , location = LocLine (Text.unpack filename) linenumber     , impact = severityText (severity metadata)     } @@ -84,14 +89,14 @@         InfoC -> "info"         StyleC -> "minor" -formatResult :: Result -> Seq Issue+formatResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> Seq Issue formatResult (Result errors checks) = allIssues   where     allIssues = errorMessages <> checkMessages     errorMessages = fmap errorToIssue errors     checkMessages = fmap checkToIssue checks -printResult :: Result -> IO ()+printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO () printResult result = mapM_ output (formatResult result)   where     output value = do
src/Hadolint/Formatter/Format.hs view
@@ -1,6 +1,5 @@ module Hadolint.Formatter.Format-    ( formatErrorReason-    , severityText+    ( severityText     , stripNewlines     , Result(..)     , toResult@@ -12,22 +11,21 @@ import Data.Sequence (Seq, fromList, singleton) import Hadolint.Rules import ShellCheck.Interface-import Text.Parsec.Error-       (ParseError, errorMessages, showErrorMessages)+import Text.Megaparsec.Error (ParseError) -data Result = Result-    { errors :: Seq ParseError+data Result t e = Result+    { errors :: Seq (ParseError t e)     , checks :: Seq RuleCheck     } deriving (Eq) -instance Semigroup Result where+instance Semigroup (Result t e) where     (Result e1 c1) <> (Result e2 c2) = Result (e1 <> e2) (c1 <> c2) -instance Monoid Result where+instance Monoid (Result t e) where     mappend = (<>)     mempty = Result mempty mempty -toResult :: Either ParseError [RuleCheck] -> Result+toResult :: Either (ParseError t e) [RuleCheck] -> Result t e toResult res =     case res of         Left err -> Result (singleton err) mempty@@ -40,16 +38,6 @@         WarningC -> "warning"         InfoC -> "info"         StyleC -> "style"--formatErrorReason :: ParseError -> String-formatErrorReason err =-    showErrorMessages-        "or"-        "unknown parse error"-        "expecting"-        "unexpected"-        "end of input"-        (errorMessages err)  stripNewlines :: String -> String stripNewlines =
src/Hadolint/Formatter/Json.hs view
@@ -8,20 +8,22 @@  import Data.Aeson hiding (Result) import qualified Data.ByteString.Lazy.Char8 as B+import qualified Data.List.NonEmpty as NE import Data.Monoid ((<>))-import Hadolint.Formatter.Format-       (Result(..), formatErrorReason, severityText)+import Hadolint.Formatter.Format (Result(..), severityText) import Hadolint.Rules (Metadata(..), RuleCheck(..)) import ShellCheck.Interface-import Text.Parsec (ParseError)-import Text.Parsec.Error (errorPos)-import Text.Parsec.Pos+import Text.Megaparsec.Error+       (ParseError, ShowErrorComponent, ShowToken, errorPos,+        parseErrorTextPretty)+import Text.Megaparsec.Pos+       (sourceColumn, sourceLine, sourceName, unPos) -data JsonFormat+data JsonFormat t e     = JsonCheck RuleCheck-    | JsonParseError ParseError+    | JsonParseError (ParseError t e) -instance ToJSON JsonFormat where+instance (ShowToken t, Ord t, ShowErrorComponent e) => ToJSON (JsonFormat t e) where     toJSON (JsonCheck RuleCheck {..}) =         object             [ "file" .= filename@@ -34,21 +36,21 @@     toJSON (JsonParseError err) =         object             [ "file" .= sourceName pos-            , "line" .= sourceLine pos-            , "column" .= sourceColumn pos+            , "line" .= unPos (sourceLine pos)+            , "column" .= unPos (sourceColumn pos)             , "level" .= severityText ErrorC             , "code" .= ("DL1000" :: String)-            , "message" .= formatErrorReason err+            , "message" .= parseErrorTextPretty err             ]       where-        pos = errorPos err+        pos = NE.head (errorPos err) -formatResult :: Result -> Value+formatResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> Value formatResult (Result errors checks) = toJSON allMessages   where     allMessages = errorMessages <> checkMessages     errorMessages = fmap JsonParseError errors     checkMessages = fmap JsonCheck checks -printResult :: Result -> IO ()+printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO () printResult result = B.putStrLn (encode (formatResult result))
src/Hadolint/Formatter/TTY.hs view
@@ -1,36 +1,43 @@ {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}  module Hadolint.Formatter.TTY     ( printResult     , formatError     ) where +import qualified Data.List.NonEmpty as NE+import Data.Semigroup ((<>))+import qualified Data.Text as Text import Hadolint.Formatter.Format import Hadolint.Rules import Language.Docker.Syntax-import Text.Parsec.Error (ParseError, errorPos)-import Text.Parsec.Pos+import Text.Megaparsec.Error+       (ParseError, ShowErrorComponent, ShowToken, errorPos,+        parseErrorTextPretty)+import Text.Megaparsec.Pos (sourcePosPretty) -formatErrors :: Functor t => t ParseError -> t String+formatErrors ::+       (ShowToken t, Ord t, ShowErrorComponent e, Functor f) => f (ParseError t e) -> f String formatErrors = fmap formatError -formatError :: ParseError -> String-formatError err = posPart ++ stripNewlines (formatErrorReason err)+formatError :: (ShowToken t, Ord t, ShowErrorComponent e) => ParseError t e -> String+formatError err = posPart ++ " " ++ stripNewlines (parseErrorTextPretty err)   where-    pos = errorPos err-    posPart = sourceName pos ++ ":" ++ show (sourceLine pos) ++ ":" ++ show (sourceColumn pos)+    pos = NE.head (errorPos err)+    posPart = sourcePosPretty pos -formatChecks :: Functor t => t RuleCheck -> t String+formatChecks :: Functor f => f RuleCheck -> f Text.Text formatChecks = fmap formatCheck   where     formatCheck (RuleCheck meta source line _) =-        formatPos source line ++ code meta ++ " " ++ message meta+        formatPos source line <> code meta <> " " <> message meta -formatPos :: Filename -> Linenumber -> String-formatPos source line = source ++ ":" ++ show line ++ " "+formatPos :: Filename -> Linenumber -> Text.Text+formatPos source line = source <> ":" <> Text.pack (show line) <> " " -printResult :: Result -> IO ()+printResult :: (ShowToken t, Ord t, ShowErrorComponent e) => Result t e -> IO () printResult Result {errors, checks} = printErrors >> printChecks   where     printErrors = mapM_ putStrLn (formatErrors errors)-    printChecks = mapM_ putStrLn (formatChecks checks)+    printChecks = mapM_ (putStrLn . Text.unpack) (formatChecks checks)
src/Hadolint/Rules.hs view
@@ -1,25 +1,28 @@ {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}  module Hadolint.Rules where  import Control.Arrow ((&&&))-import Data.List-       (dropWhile, isInfixOf, isPrefixOf, isSuffixOf, mapAccumL)+import Data.List (dropWhile, isInfixOf, isPrefixOf, mapAccumL) import Data.List.NonEmpty (toList) import Data.List.Split (splitOneOf) import Hadolint.Bash import Language.Docker.Syntax +import Data.Semigroup ((<>)) import qualified Data.Set as Set+import qualified Data.Text as Text+import Data.Void (Void) import qualified ShellCheck.Interface import ShellCheck.Interface (Severity(..))-import qualified Text.Parsec as Parsec-import Text.Parsec ((<|>))+import qualified Text.Megaparsec as Megaparsec+import qualified Text.Megaparsec.Char as Megaparsec  data Metadata = Metadata-    { code :: String+    { code :: Text.Text     , severity :: Severity-    , message :: String+    , message :: Text.Text     } deriving (Eq)  -- a check is the application of a rule on a specific part of code@@ -37,10 +40,12 @@ instance Ord RuleCheck where     a `compare` b = linenumber a `compare` linenumber b -link :: Metadata -> String+type IgnoreRuleParser = Megaparsec.Parsec Void Text.Text++link :: Metadata -> Text.Text link (Metadata code _ _)-    | "SC" `isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" ++ code-    | "DL" `isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" ++ code+    | "SC" `Text.isPrefixOf` code = "https://github.com/koalaman/shellcheck/wiki/" <> code+    | "DL" `Text.isPrefixOf` code = "https://github.com/hadolint/hadolint/wiki/" <> code     | otherwise = "https://github.com/hadolint/hadolint"  -- a Rule takes a Dockerfile and returns the executed checks@@ -49,7 +54,7 @@ -- Apply a function on each instruction and create a check -- for the according line number mapInstructions ::-       Metadata -> (state -> Linenumber -> Instruction -> (state, Bool)) -> state -> Rule+       Metadata -> (state -> Linenumber -> Instruction Text.Text -> (state, Bool)) -> state -> Rule mapInstructions metadata f initialState dockerfile =     let (_, results) = mapAccumL applyRule initialState dockerfile     in results@@ -58,21 +63,22 @@         let (newState, res) = f state linenumber i         in (newState, RuleCheck metadata source linenumber res) -instructionRule :: String -> Severity -> String -> (Instruction -> Bool) -> Rule+instructionRule :: Text.Text -> Severity -> Text.Text -> (Instruction Text.Text -> Bool) -> Rule instructionRule code severity message check =     instructionRuleLine code severity message (const check) -instructionRuleLine :: String -> Severity -> String -> (Linenumber -> Instruction -> Bool) -> Rule+instructionRuleLine ::+       Text.Text -> Severity -> Text.Text -> (Linenumber -> Instruction Text.Text -> Bool) -> Rule instructionRuleLine code severity message check =     instructionRuleState code severity message checkAndDropState ()   where     checkAndDropState state line instr = (state, check line instr)  instructionRuleState ::-       String+       Text.Text     -> Severity-    -> String-    -> (state -> Linenumber -> Instruction -> (state, Bool))+    -> Text.Text+    -> (state -> Linenumber -> Instruction Text.Text -> (state, Bool))     -> state     -> Rule instructionRuleState code severity message = mapInstructions (Metadata code severity message)@@ -80,6 +86,14 @@ withState :: a -> b -> (a, b) withState st res = (st, res) +argumentsRule :: ([String] -> a) -> Arguments Text.Text -> a+argumentsRule applyRule args =+    case args of+        ArgumentsText as -> applyRule . normalizeArgs $ as+        ArgumentsList as -> applyRule . normalizeArgs $ as+  where+    normalizeArgs = words . Text.unpack+ -- Enforce rules on a dockerfile and return failed checks analyze :: [Rule] -> Dockerfile -> [RuleCheck] analyze list dockerfile = filter failed $ concat [r dockerfile | r <- list]@@ -89,25 +103,29 @@     wasIgnored c ln = not $ null [line | (line, codes) <- allIgnores, line == ln, c `elem` codes]     allIgnores = ignored dockerfile -ignored :: Dockerfile -> [(Linenumber, [String])]+ignored :: Dockerfile -> [(Linenumber, [Text.Text])] ignored dockerfile =     [(l + 1, ignores) | (l, Just ignores) <- map (lineNumber &&& extractIgnored) dockerfile]   where     extractIgnored = ignoreFromInstruction . instruction-    ignoreFromInstruction (Comment comment) = either (const Nothing) Just (parseComment comment)+    ignoreFromInstruction (Comment comment) = parseComment comment     ignoreFromInstruction _ = Nothing     -- | Parses the comment text and extracts the ignored rule names-    parseComment :: String -> Either Parsec.ParseError [String]-    parseComment = Parsec.parse commentParser ""+    parseComment :: Text.Text -> Maybe [Text.Text]+    parseComment = Megaparsec.parseMaybe commentParser+    commentParser :: IgnoreRuleParser [Text.Text]     commentParser =-        Parsec.skipMany space >> -- The parser for the ignored rules-        Parsec.string "hadolint" >>-        Parsec.skipMany1 space >>-        Parsec.string "ignore=" >>-        Parsec.skipMany space >>-        Parsec.sepBy1 ruleName (Parsec.many space >> Parsec.char ',' >> Parsec.many space)-    space = Parsec.char ' ' <|> Parsec.char '\t'-    ruleName = Parsec.many1 (Parsec.choice $ map Parsec.char "DLSC0123456789")+        spaces >> -- The parser for the ignored rules+        string "hadolint" >>+        spaces1 >>+        string "ignore=" >>+        spaces >>+        Megaparsec.sepBy1 ruleName (spaces >> string "," >> spaces)+    string = Megaparsec.string+    spaces = Megaparsec.takeWhileP Nothing space+    spaces1 = Megaparsec.takeWhile1P Nothing space+    space c = c == ' ' || c == '\t'+    ruleName = Megaparsec.takeWhile1P Nothing (\c -> c `elem` ("DLSC0123456789" :: String))  rules :: [Rule] rules =@@ -145,14 +163,16 @@  commentMetadata :: ShellCheck.Interface.Comment -> Metadata commentMetadata (ShellCheck.Interface.Comment severity code message) =-    Metadata ("SC" ++ show code) severity message+    Metadata (Text.pack ("SC" ++ show code)) severity (Text.pack message)  shellcheckBash :: Dockerfile -> [RuleCheck] shellcheckBash = concatMap check   where-    check (InstructionPos (Run (Arguments args)) source linenumber) =-        rmDup [RuleCheck m source linenumber False | m <- convert args]+    check (InstructionPos (Run args) source linenumber) =+        argumentsRule (applyRule source linenumber) args     check _ = []+    applyRule source linenumber args =+        rmDup [RuleCheck m source linenumber False | m <- convert args]     convert args = [commentMetadata c | c <- shellcheck $ unwords args]     rmDup :: [RuleCheck] -> [RuleCheck]     rmDup [] = []@@ -173,19 +193,19 @@   where     extractAlias (l, f) = (l, fromAlias f) -allImageNames :: Dockerfile -> [(Linenumber, String)]+allImageNames :: Dockerfile -> [(Linenumber, Text.Text)] allImageNames dockerfile = [(l, fromName baseImage) | (l, baseImage) <- allFromImages dockerfile]  -- | Returns a list of all image aliases in FROM instructions that --  are defined before the given line number.-previouslyDefinedAliases :: Linenumber -> Dockerfile -> [String]+previouslyDefinedAliases :: Linenumber -> Dockerfile -> [Text.Text] previouslyDefinedAliases line dockerfile =     [i | (l, ImageAlias i) <- allAliasedImages dockerfile, l < line]  -- | Returns the result of running the check function on the image alias --   name, if the passed instruction is a FROM instruction with a stage alias. --   Otherwise, returns True.-aliasMustBe :: (String -> Bool) -> Instruction -> Bool+aliasMustBe :: (Text.Text -> Bool) -> Instruction a -> Bool aliasMustBe predicate fromInstr =     case fromInstr of         From (UntaggedImage _ (Just (ImageAlias alias))) -> predicate alias@@ -193,7 +213,7 @@         From (DigestedImage _ _ (Just (ImageAlias alias))) -> predicate alias         _ -> True -fromName :: BaseImage -> String+fromName :: BaseImage -> Text.Text fromName (UntaggedImage Image {imageName} _) = imageName fromName (TaggedImage Image {imageName} _ _) = imageName fromName (DigestedImage Image {imageName} _ _) = imageName@@ -209,9 +229,10 @@     code = "DL3000"     severity = ErrorC     message = "Use absolute WORKDIR"-    check (Workdir ('$':_)) = True-    check (Workdir ('/':_)) = True-    check (Workdir _) = False+    check (Workdir loc)+        | "$" `Text.isPrefixOf` loc = True+        | "/" `Text.isPrefixOf` loc = True+        | otherwise = False     check _ = True  hasNoMaintainer :: Rule@@ -257,7 +278,7 @@     code = "DL4001"     severity = WarningC     message = "Either use Wget or Curl but not both"-    check state _ (Run (Arguments args)) = detectDoubleUsage state args+    check state _ (Run args) = argumentsRule (detectDoubleUsage state) args     check state _ _ = withState state True     detectDoubleUsage state args =         let newArgs = extractCommands args@@ -273,8 +294,10 @@     message =         "For some bash commands it makes no sense running them in a Docker container like `ssh`, \         \`vim`, `shutdown`, `service`, `ps`, `free`, `top`, `kill`, `mount`, `ifconfig`"-    check (Run (Arguments (arg:_))) = arg `notElem` invalidCmds+    check (Run args) = argumentsRule applyRule args     check _ = True+    applyRule (arg:_) = arg `notElem` invalidCmds+    applyRule _ = True     invalidCmds = ["ssh", "vim", "shutdown", "service", "ps", "free", "top", "kill", "mount"]  noRootUser :: Rule@@ -284,7 +307,9 @@     severity = WarningC     message = "Do not switch to root USER"     check (User user) =-        not (isPrefixOf "root:" user || isPrefixOf "0:" user || user == "root" || user == "0")+        not+            (Text.isPrefixOf "root:" user ||+             Text.isPrefixOf "0:" user || user == "root" || user == "0")     check _ = True  noCd :: Rule@@ -293,7 +318,7 @@     code = "DL3003"     severity = WarningC     message = "Use WORKDIR to switch to a directory"-    check (Run (Arguments args)) = not $ usingProgram "cd" args+    check (Run args) = argumentsRule (not . usingProgram "cd") args     check _ = True  noSudo :: Rule@@ -304,7 +329,7 @@     message =         "Do not use sudo as it leads to unpredictable behavior. Use a tool like gosu to enforce \         \root"-    check (Run (Arguments args)) = not $ usingProgram "sudo" args+    check (Run args) = argumentsRule (not . usingProgram "sudo") args     check _ = True  noAptGetUpgrade :: Rule@@ -313,7 +338,7 @@     code = "DL3005"     severity = ErrorC     message = "Do not use apt-get upgrade or dist-upgrade"-    check (Run (Arguments args)) = not $ isInfixOf ["apt-get", "upgrade"] args+    check (Run args) = argumentsRule (not . isInfixOf ["apt-get", "upgrade"]) args     check _ = True  noUntagged :: Rule@@ -346,7 +371,7 @@     message =         "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get \         \install <package>=<version>`"-    check (Run (Arguments args)) = and [versionFixed p | p <- aptGetPackages args]+    check (Run args) = argumentsRule (\as -> and [versionFixed p | p <- aptGetPackages as]) args     check _ = True     versionFixed package = "=" `isInfixOf` package @@ -372,10 +397,12 @@     --   We only care for users to delete the lists folder if the FROM clase we're is is the last one     --   or if it is used as the base image for another FROM clause.     check _ line f@(From _) = withState (Just (line, f)) True -- Remember the last FROM instruction found-    check st@(Just (line, From baseimage)) _ (Run (Arguments args))-        | not (hasUpdate args) || not (imageIsUsed line baseimage) = withState st True-        | otherwise = withState st (hasCleanup args)+    check st@(Just (line, From baseimage)) _ (Run args) =+        withState st (argumentsRule (applyRule line baseimage) args)     check st _ _ = withState st True+    applyRule line baseimage args+        | not (hasUpdate args) || not (imageIsUsed line baseimage) = True+        | otherwise = hasCleanup args     hasCleanup cmd = ["rm", "-rf", "/var/lib/apt/lists/*"] `isInfixOf` cmd     hasUpdate cmd = ["apt-get", "update"] `isInfixOf` cmd     imageIsUsed line baseimage = isLastImage line baseimage || imageIsUsedLater line baseimage@@ -401,7 +428,7 @@     code = "DL3017"     severity = ErrorC     message = "Do not use apk upgrade"-    check (Run (Arguments args)) = not $ isInfixOf ["apk", "upgrade"] args+    check (Run args) = argumentsRule (not . isInfixOf ["apk", "upgrade"]) args     check _ = True  isApkAdd :: [String] -> Bool@@ -414,7 +441,7 @@     severity = WarningC     message =         "Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"-    check (Run (Arguments args)) = and [versionFixed p | p <- apkAddPackages args]+    check (Run args) = argumentsRule (\as -> and [versionFixed p | p <- apkAddPackages as]) args     check _ = True     versionFixed package = "=" `isInfixOf` package @@ -437,7 +464,7 @@     message =         "Use the `--no-cache` switch to avoid the need to use `--update` and remove \         \`/var/cache/apk/*` when done installing packages"-    check (Run (Arguments args)) = not (isApkAdd args) || hasNoCacheOption args+    check (Run args) = argumentsRule (\as -> not (isApkAdd as) || hasNoCacheOption as) args     check _ = True     hasNoCacheOption cmd = ["--no-cache"] `isInfixOf` cmd @@ -449,7 +476,7 @@     message = "Use ADD for extracting archives into an image"     check (Copy (CopyArgs srcs _ _ _)) =         and-            [ not (format `isSuffixOf` src)+            [ not (format `Text.isSuffixOf` src)             | SourcePath src <- toList srcs             , format <- archiveFormats             ]@@ -475,7 +502,8 @@     message =         "Pin versions in pip. Instead of `pip install <package>` use `pip install \         \<package>==<version>`"-    check (Run (Arguments args)) = not (isPipInstall args) || all versionFixed (packages args)+    check (Run args) =+        argumentsRule (\as -> not (isPipInstall as) || all versionFixed (packages as)) args     check _ = True     isPipInstall :: [String] -> Bool     isPipInstall cmd =@@ -533,7 +561,7 @@     message =         "Pin versions in npm. Instead of `npm install <package>` use `npm install \         \<package>@<version>`"-    check (Run (Arguments args)) = all versionFixed (packages args)+    check (Run args) = argumentsRule (all versionFixed . packages) args     check _ = True     packages cmd =         case getInstallArgs cmd of@@ -563,7 +591,7 @@     code = "DL3014"     severity = WarningC     message = "Use the `-y` switch to avoid manual input `apt-get -y install <package>`"-    check (Run (Arguments args)) = not (isAptGetInstall args) || hasYesOption args+    check (Run args) = argumentsRule (\as -> not (isAptGetInstall as) || hasYesOption as) args     check _ = True     hasYesOption cmd =         ["-y"] `isInfixOf` cmd ||@@ -576,14 +604,15 @@     code = "DL3015"     severity = InfoC     message = "Avoid additional packages by specifying `--no-install-recommends`"-    check (Run (Arguments args)) = not (isAptGetInstall args) || hasNoRecommendsOption args+    check (Run args) =+        argumentsRule (\as -> not (isAptGetInstall as) || hasNoRecommendsOption as) args     check _ = True     hasNoRecommendsOption cmd = ["--no-install-recommends"] `isInfixOf` cmd -isArchive :: String -> Bool+isArchive :: Text.Text -> Bool isArchive path =     True `elem`-    [ ftype `isSuffixOf` path+    [ ftype `Text.isSuffixOf` path     | ftype <-           [ ".tar"           , ".gz"@@ -603,8 +632,8 @@           ]     ] -isUrl :: String -> Bool-isUrl path = True `elem` [proto `isPrefixOf` path | proto <- ["https://", "http://"]]+isUrl :: Text.Text -> Bool+isUrl path = True `elem` [proto `Text.isPrefixOf` path | proto <- ["https://", "http://"]]  copyInsteadAdd :: Rule copyInsteadAdd = instructionRule code severity message check@@ -626,7 +655,7 @@         | length sources > 1 = endsWithSlash t         | otherwise = True     check _ = True-    endsWithSlash (TargetPath t) = last t == '/' -- it is safe to use last, as the target is never empty+    endsWithSlash (TargetPath t) = Text.last t == '/' -- it is safe to use last, as the target is never empty  copyFromExists :: Rule copyFromExists dockerfile = instructionRuleLine code severity message check dockerfile@@ -666,6 +695,6 @@     code = "DL4005"     severity = WarningC     message = "Use SHELL to change the default shell"-    check (Run (Arguments args)) = not $ any shellSymlink (bashCommands args)+    check (Run args) = argumentsRule (not . any shellSymlink . bashCommands) args     check _ = True     shellSymlink args = usingProgram "ln" args && isInfixOf ["/bin/sh"] args
test/Spec.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} import Test.HUnit hiding (Label) import Test.Hspec @@ -5,6 +6,8 @@ import Hadolint.Rules  import Language.Docker.Parser+import Data.Semigroup ((<>))+import qualified Data.Text as Text  main :: IO () main =@@ -30,7 +33,7 @@                         , "FROM alpine:3.7"                         , "RUN baz"                         ]-                in ruleCatchesNot noUntagged $ unlines dockerFile+                in ruleCatchesNot noUntagged $ Text.unlines dockerFile             it "other untagged cases are not ok" $                 let dockerFile =                         [ "FROM golang:1.9.3-alpine3.7 AS build"@@ -40,7 +43,7 @@                         , "FROM alpine:3.7"                         , "RUN baz"                         ]-                in ruleCatches noUntagged $ unlines dockerFile+                in ruleCatches noUntagged $ Text.unlines dockerFile         --         describe "no root or sudo rules" $ do             it "sudo" $ ruleCatches noSudo "RUN sudo apt-get update"@@ -66,7 +69,7 @@                         [ "FROM scratch"                         , "RUN apt-get update && apt-get install python"                         ]-                in ruleCatches aptGetCleanup $ unlines dockerFile+                in ruleCatches aptGetCleanup $ Text.unlines dockerFile             it "apt-get cleanup in stage image" $                 let dockerFile =                         [ "FROM ubuntu as foo"@@ -74,7 +77,7 @@                         , "FROM scratch"                         , "RUN echo hey!"                         ]-                in ruleCatchesNot aptGetCleanup $ unlines dockerFile+                in ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile             it "apt-get no cleanup in last stage" $                 let dockerFile =                         [ "FROM ubuntu as foo"@@ -82,7 +85,7 @@                         , "FROM scratch"                         , "RUN apt-get update && apt-get install python"                         ]-                in ruleCatches aptGetCleanup $ unlines dockerFile+                in ruleCatches aptGetCleanup $ Text.unlines dockerFile             it "apt-get no cleanup in intermediate stage" $                 let dockerFile =                         [ "FROM ubuntu as foo"@@ -90,7 +93,7 @@                         , "FROM foo"                         , "RUN hey!"                         ]-                in ruleCatches aptGetCleanup $ unlines dockerFile+                in ruleCatches aptGetCleanup $ Text.unlines dockerFile             it "now warn apt-get cleanup in intermediate stage that cleans lists" $                 let dockerFile =                         [ "FROM ubuntu as foo"@@ -98,7 +101,7 @@                         , "FROM foo"                         , "RUN hey!"                         ]-                in ruleCatchesNot aptGetCleanup $ unlines dockerFile+                in ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile             it "no warn apt-get cleanup in intermediate stage when stage not used later" $                 let dockerFile =                         [ "FROM ubuntu as foo"@@ -106,13 +109,13 @@                         , "FROM scratch"                         , "RUN hey!"                         ]-                in ruleCatchesNot aptGetCleanup $ unlines dockerFile+                in ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile             it "apt-get cleanup" $                 let dockerFile =                         [ "FROM scratch"                         , "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"                         ]-                in ruleCatchesNot aptGetCleanup $ unlines dockerFile+                in ruleCatchesNot aptGetCleanup $ Text.unlines dockerFile              it "apt-get pinned chained" $                 let dockerFile =@@ -120,7 +123,7 @@                         , " && apt-get -yqq --no-install-recommends install nodejs=0.10 \\"                         , " && rm -rf /var/lib/apt/lists/*"                         ]-                in ruleCatchesNot aptGetVersionPinned $ unlines dockerFile+                in ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile             it "apt-get pinned regression" $                 let dockerFile =                         [ "RUN apt-get update && apt-get install --no-install-recommends -y \\"@@ -129,7 +132,7 @@                         , "git=1:2.5.0* \\"                         , "ruby=1:2.1.*"                         ]-                in ruleCatchesNot aptGetVersionPinned $ unlines dockerFile+                in ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile             it "has deprecated maintainer" $                 ruleCatches hasNoMaintainer "FROM busybox\nMAINTAINER hudu@mail.com"         --@@ -143,7 +146,7 @@                         [ "RUN apk add --no-cache flex=2.6.4-r1 \\"                         , " && pip install -r requirements.txt"                         ]-                in ruleCatchesNot apkAddVersionPinned $ unlines dockerFile+                in ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile             it "apk add version pinned regression" $                 let dockerFile =                         [ "RUN apk add --no-cache \\"@@ -152,7 +155,7 @@                         , "python2=2.7.13-r1 \\"                         , "libbz2=1.0.6-r5"                         ]-                in ruleCatchesNot apkAddVersionPinned $ unlines dockerFile+                in ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile             it "apk add version pinned regression - one missed" $                 let dockerFile =                         [ "RUN apk add --no-cache \\"@@ -161,7 +164,7 @@                         , "python2=2.7.13-r1 \\"                         , "libbz2=1.0.6-r5"                         ]-                in ruleCatches apkAddVersionPinned $ unlines dockerFile+                in ruleCatches apkAddVersionPinned $ Text.unlines dockerFile             it "apk add with --no-cache" $ ruleCatches apkAddNoCache "RUN apk add flex=2.6.4-r1"             it "apk add without --no-cache" $                 ruleCatchesNot apkAddNoCache "RUN apk add --no-cache flex=2.6.4-r1"@@ -174,7 +177,7 @@                         , "&& python setup.py install \\"                         , "&& apk del build-dependencies"                         ]-                in ruleCatchesNot apkAddVersionPinned $ unlines dockerFile+                in ruleCatchesNot apkAddVersionPinned $ Text.unlines dockerFile         --         describe "EXPOSE rules" $ do             it "invalid port" $ ruleCatches invalidPort "EXPOSE 80000"@@ -317,7 +320,7 @@                           \openjdk-8-jdk=8u131-b11-1~bpo8+1 &&\\"                         , " rm -rf /var/lib/apt/lists/*"                         ]-                in ruleCatchesNot aptGetVersionPinned $ unlines dockerFile+                in ruleCatchesNot aptGetVersionPinned $ Text.unlines dockerFile              it "has maintainer" $ ruleCatches hasNoMaintainer "FROM debian\nMAINTAINER Lukas"             it "has maintainer first" $ ruleCatches hasNoMaintainer "MAINTAINER Lukas\nFROM DEBIAN"@@ -356,7 +359,7 @@                         , "FROM node as build"                         , "RUN baz"                         ]-                in ruleCatches copyFromExists $ unlines dockerFile+                in ruleCatches copyFromExists $ Text.unlines dockerFile             it "don't warn on correctly defined aliases" $                 let dockerFile =                         [ "FROM scratch as build"@@ -365,7 +368,7 @@                         , "COPY --from=build foo ."                         , "RUN baz"                         ]-                in ruleCatchesNot copyFromExists $ unlines dockerFile+                in ruleCatchesNot copyFromExists $ Text.unlines dockerFile         --         describe "copy from own FROM" $ do             it "warn on copying from your the same FROM" $@@ -373,7 +376,7 @@                         [ "FROM node as foo"                         , "COPY --from=foo bar ."                         ]-                in ruleCatches copyFromAnother $ unlines dockerFile+                in ruleCatches copyFromAnother $ Text.unlines dockerFile             it "don't warn on copying form other sources" $                 let dockerFile =                         [ "FROM scratch as build"@@ -382,7 +385,7 @@                         , "COPY --from=build foo ."                         , "RUN baz"                         ]-                in ruleCatchesNot copyFromAnother $ unlines dockerFile+                in ruleCatchesNot copyFromAnother $ Text.unlines dockerFile         --         describe "Duplicate aliases" $ do             it "warn on duplicate aliases" $@@ -392,7 +395,7 @@                         , "FROM scratch as foo"                         , "RUN something"                         ]-                in ruleCatches fromAliasUnique $ unlines dockerFile+                in ruleCatches fromAliasUnique $ Text.unlines dockerFile             it "don't warn on unique aliases" $                 let dockerFile =                         [ "FROM scratch as build"@@ -400,12 +403,14 @@                         , "FROM node as run"                         , "RUN baz"                         ]-                in ruleCatchesNot fromAliasUnique $ unlines dockerFile+                in ruleCatchesNot fromAliasUnique $ Text.unlines dockerFile         --         describe "format error" $             it "display error after line pos" $ do-                let ast = parseString "FOM debian:jessie"-                    expectedMsg = "<string>:1:2 unexpected \"O\" expecting FROM"+                let ast = parseText "FOM debian:jessie"+                    expectedMsg = "<string>:1:1 unexpected 'F' expecting '#', ADD, ARG, CMD, COPY, ENTRYPOINT, " <>+                                  "ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, " <>+                                  "USER, VOLUME, WORKDIR, or end of input "                 case ast of                     Left err -> assertEqual "Unexpected error msg" expectedMsg (formatError err)                     Right _ -> assertFailure "AST should fail parsing"@@ -417,32 +422,32 @@                         , "# hadolint ignore=DL3002"                         , "USER root"                         ]-                in ruleCatchesNot noRootUser $ unlines dockerFile+                in ruleCatchesNot noRootUser $ Text.unlines dockerFile             it "ignores only the given rule" $                 let dockerFile =                         [ "# hadolint ignore=DL3001"                         , "USER root"                         ]-                in ruleCatches noRootUser $ unlines dockerFile+                in ruleCatches noRootUser $ Text.unlines dockerFile             it "ignores only the given rule, when multiple passed" $                 let dockerFile =                         [ "# hadolint ignore=DL3001,DL3002"                         , "USER root"                         ]-                in ruleCatchesNot noRootUser $ unlines dockerFile+                in ruleCatchesNot noRootUser $ Text.unlines dockerFile             it "ignores the rule only if directly above the instruction" $                 let dockerFile =                         [ "# hadolint ignore=DL3001,DL3002"                         , "FROM ubuntu"                         , "USER root"                         ]-                in ruleCatches noRootUser $ unlines dockerFile+                in ruleCatches noRootUser $ Text.unlines dockerFile             it "won't ignore the rule if passed invalid rule names" $                 let dockerFile =                         [ "# hadolint ignore=crazy,DL3002"                         , "USER root"                         ]-                in ruleCatches noRootUser $ unlines dockerFile+                in ruleCatches noRootUser $ Text.unlines dockerFile             it "ignores multiple rules correctly, even with some extra whitespace" $                 let dockerFile =                         [ "FROM node as foo"@@ -450,24 +455,24 @@                         , "COPY --from=foo bar baz ."                         ]                 in do-                  ruleCatchesNot copyFromAnother $ unlines dockerFile-                  ruleCatchesNot copyEndingSlash $ unlines dockerFile+                  ruleCatchesNot copyFromAnother $ Text.unlines dockerFile+                  ruleCatchesNot copyEndingSlash $ Text.unlines dockerFile -assertChecks :: HasCallStack => Rule -> String -> ([RuleCheck] -> IO a) -> IO a+assertChecks :: HasCallStack => Rule -> Text.Text -> ([RuleCheck] -> IO a) -> IO a assertChecks rule s makeAssertions =-    case parseString (s ++ "\n") of+    case parseText (s <> "\n") of         Left err -> assertFailure $ show err         Right dockerFile -> makeAssertions $ analyze [rule] dockerFile  -- Assert a failed check exists for rule-ruleCatches :: HasCallStack => Rule -> String -> Assertion+ruleCatches :: HasCallStack => Rule -> Text.Text -> Assertion ruleCatches rule s = assertChecks rule s f   where     f checks = do       assertEqual "No check for rule found" 1 $ length checks       assertBool "Incorrect line number for result" $ null [c | c <- checks, linenumber c <= 0] -ruleCatchesNot :: HasCallStack => Rule -> String -> Assertion+ruleCatchesNot :: HasCallStack => Rule -> Text.Text -> Assertion ruleCatchesNot rule s = assertChecks rule s f   where     f checks = assertEqual "Found check of rule" 0 $ length checks