packages feed

hadolint-2.9.2: src/Hadolint/Rule/DL3013.hs

module Hadolint.Rule.DL3013 (rule) where

import Data.List (isInfixOf)
import qualified Data.Text as Text
import Hadolint.Rule
import Hadolint.Shell (ParsedShell)
import qualified Hadolint.Shell as Shell
import Language.Docker.Syntax


rule :: Rule ParsedShell
rule = dl3013 <> onbuild dl3013
{-# INLINEABLE rule #-}

dl3013 :: Rule ParsedShell
dl3013 = simpleRule code severity message check
  where
    code = "DL3013"
    severity = DLWarningC
    message =
      "Pin versions in pip. Instead of `pip install <package>` use `pip install \
      \<package>==<version>` or `pip install --requirement <requirements file>`"
    check (Run (RunArgs args _)) = foldArguments (Shell.noCommands forgotToPinVersion) args
    check _ = True

    forgotToPinVersion cmd =
      isPipInstall cmd
        && not (hasBuildConstraint cmd)
        && not (all versionFixed (packages cmd))

    -- Check if the command is a pip* install command, and that specific packages are being listed
    isPipInstall cmd =
      ( Shell.isPipInstall cmd
          && not (hasBuildConstraint cmd)
          && not (all versionFixed (packages cmd))
      )
        && not (requirementInstall cmd)

    -- If the user is installing requirements from a file or just the local module, then we are not interested
    -- in running this rule
    requirementInstall cmd =
      ["--requirement"] `isInfixOf` Shell.getArgs cmd
        || ["-r"] `isInfixOf` Shell.getArgs cmd
        || ["."] `isInfixOf` Shell.getArgs cmd

    hasBuildConstraint cmd = Shell.hasFlag "constraint" cmd || Shell.hasFlag "c" cmd
    versionFixed package = hasVersionSymbol package || isVersionedGit package || isLocalPackage package
    isVersionedGit package = "git+http" `Text.isInfixOf` package && "@" `Text.isInfixOf` package
    versionSymbols = ["==", ">=", "<=", ">", "<", "!=", "~=", "==="]
    hasVersionSymbol package = or [s `Text.isInfixOf` package | s <- versionSymbols]
    localPackageFileExtensions = [".whl", ".tar.gz"]
    isLocalPackage package = or [s `Text.isSuffixOf` package | s <- localPackageFileExtensions]
{-# INLINEABLE dl3013 #-}

packages :: Shell.Command -> [Text.Text]
packages cmd =
  stripInstallPrefix $
    Shell.getArgsNoFlags $
      Shell.dropFlagArg
        [ "abi",
          "b",
          "build",
          "e",
          "editable",
          "extra-index-url",
          "f",
          "find-links",
          "i",
          "index-url",
          "implementation",
          "no-binary",
          "only-binary",
          "platform",
          "prefix",
          "progress-bar",
          "proxy",
          "python-version",
          "root",
          "src",
          "t",
          "target",
          "trusted-host",
          "upgrade-strategy"
        ]
        cmd

stripInstallPrefix :: [Text.Text] -> [Text.Text]
stripInstallPrefix cmd = dropWhile (== "install") (dropWhile (/= "install") cmd)