content: {-
      Copyright 2012-2022 Vidar Holen
  
      This file is part of ShellCheck.
      https://www.shellcheck.net
  
      ShellCheck is free software: you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
      the Free Software Foundation, either version 3 of the License, or
      (at your option) any later version.
  
      ShellCheck is distributed in the hope that it will be useful,
      but WITHOUT ANY WARRANTY; without even the implied warranty of
      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      GNU General Public License for more details.
  
      You should have received a copy of the GNU General Public License
      along with this program.  If not, see <https://www.gnu.org/licenses/>.
  -}
  {-# LANGUAGE TemplateHaskell #-}
  module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
  
  import ShellCheck.Analyzer
  import ShellCheck.ASTLib
  import ShellCheck.Interface
  import ShellCheck.Parser
  
  import Debug.Trace -- DO NOT SUBMIT
  import Data.Either
  import Data.Functor
  import Data.List
  import Data.Maybe
  import Data.Ord
  import Control.Monad.Identity
  import qualified Data.Map as Map
  import qualified System.IO
  import Prelude hiding (readFile)
  import Control.Monad
  
  import Test.QuickCheck.All
  
  tokenToPosition startMap t = fromMaybe fail $ do
      span <- Map.lookup (tcId t) startMap
      return $ newPositionedComment {
          pcStartPos = fst span,
          pcEndPos = snd span,
          pcComment = tcComment t,
          pcFix = tcFix t
      }
    where
      fail = error "Internal shellcheck error: id doesn't exist. Please report!"
  
  shellFromFilename filename = listToMaybe candidates
    where
      shellExtensions = [(".ksh", Ksh)
                        ,(".bash", Bash)
                        ,(".bats", Bash)
                        ,(".dash", Dash)]
                        -- The `.sh` is too generic to determine the shell:
                        -- We fallback to Bash in this case and emit SC2148 if there is no shebang
      candidates =
          [sh | (ext,sh) <- shellExtensions, ext `isSuffixOf` filename]
  
  checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
  checkScript sys spec = do
      results <- checkScript (csScript spec)
      return emptyCheckResult {
          crFilename = csFilename spec,
          crComments = results
      }
    where
      checkScript contents = do
          result <- parseScript sys newParseSpec {
              psFilename = csFilename spec,
              psScript = contents,
              psCheckSourced = csCheckSourced spec,
              psIgnoreRC = csIgnoreRC spec,
              psShellTypeOverride = csShellTypeOverride spec
          }
          let parseMessages = prComments result
          let tokenPositions = prTokenPositions result
          let analysisSpec root =
                  as {
                      asScript = root,
                      asShellType = csShellTypeOverride spec,
                      asFallbackShell = shellFromFilename $ csFilename spec,
                      asCheckSourced = csCheckSourced spec,
                      asExecutionMode = Executed,
                      asTokenPositions = tokenPositions,
                      asExtendedAnalysis = csExtendedAnalysis spec,
                      asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
                  } where as = newAnalysisSpec root
          let analysisMessages =
                  maybe []
                      (arComments . analyzeScript . analysisSpec)
                          $ prRoot result
          let translator = tokenToPosition tokenPositions
          return . nub . sortMessages . filter shouldInclude $
              (parseMessages ++ map translator analysisMessages)
  
      shouldInclude pc =
              severity <= csMinSeverity spec &&
              case csIncludedWarnings spec of
                  Nothing -> code `notElem` csExcludedWarnings spec
                  Just includedWarnings -> code `elem` includedWarnings
          where
              code     = cCode (pcComment pc)
              severity = cSeverity (pcComment pc)
  
      sortMessages = sortOn order
      order pc =
          let pos = pcStartPos pc
              comment = pcComment pc in
          (posFile pos,
           posLine pos,
           posColumn pos,
           cSeverity comment,
           cCode comment,
           cMessage comment)
      getPosition = pcStartPos
  
  
  getErrors sys spec =
      sort . map getCode . crComments $
          runIdentity (checkScript sys spec)
    where
      getCode = cCode . pcComment
  
  check = checkWithIncludes []
  
  checkWithSpec includes =
      getErrors (mockedSystemInterface includes)
  
  checkWithIncludes includes src =
      checkWithSpec includes emptyCheckSpec {
          csScript = src,
          csExcludedWarnings = [2148]
      }
  
  checkRecursive includes src =
      checkWithSpec includes emptyCheckSpec {
          csScript = src,
          csExcludedWarnings = [2148],
          csCheckSourced = True
      }
  
  checkOptionIncludes includes src =
      checkWithSpec [] emptyCheckSpec {
          csScript = src,
          csIncludedWarnings = includes,
          csCheckSourced = True
      }
  
  checkWithRc rc = getErrors
      (mockRcFile rc $ mockedSystemInterface [])
  
  checkWithIncludesAndSourcePath includes mapper = getErrors
      (mockedSystemInterface includes) {
          siFindSource = mapper
      }
  
  checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
      (mockRcFile rc $ mockedSystemInterface includes) {
          siFindSource = mapper
      }
  
  prop_findsParseIssue = check "echo \"$12\"" == [1037]
  
  prop_commentDisablesParseIssue1 =
      null $ check "#shellcheck disable=SC1037\necho \"$12\""
  prop_commentDisablesParseIssue2 =
      null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
  
  prop_findsAnalysisIssue =
      check "echo $1" == [2086]
  prop_commentDisablesAnalysisIssue1 =
      null $ check "#shellcheck disable=SC2086\necho $1"
  prop_commentDisablesAnalysisIssue2 =
      null $ check "#shellcheck disable=SC2086\n#lol\necho $1"
  
  prop_optionDisablesIssue1 =
      null $ getErrors
                  (mockedSystemInterface [])
                  emptyCheckSpec {
                      csScript = "echo $1",
                      csExcludedWarnings = [2148, 2086]
                  }
  
  prop_optionDisablesIssue2 =
      null $ getErrors
                  (mockedSystemInterface [])
                  emptyCheckSpec {
                      csScript = "echo \"$10\"",
                      csExcludedWarnings = [2148, 1037]
                  }
  
  prop_wontParseBadShell =
      [1071] == check "#!/usr/bin/python\ntrue $1\n"
  
  prop_optionDisablesBadShebang =
      null $ getErrors
                  (mockedSystemInterface [])
                  emptyCheckSpec {
                      csScript = "#!/usr/bin/python\ntrue\n",
                      csShellTypeOverride = Just Sh
                  }
  
  prop_annotationDisablesBadShebang =
      null $ check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
  
  
  prop_canParseDevNull =
      null $ check "source /dev/null"
  
  prop_failsWhenNotSourcing =
      [1091, 2154] == check "source lol; echo \"$bar\""
  
  prop_worksWhenSourcing =
      null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
  
  prop_worksWhenSourcingWithDashDash =
      null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\""
  
  prop_worksWhenSourcingWithDashP =
      null $ checkWithIncludes [("lib", "bar=1")] "source -p \"$MYPATH\" lib; echo \"$bar\""
  
  prop_worksWhenDotting =
      null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
  
  -- FIXME: This should really be giving [1093], "recursively sourced"
  prop_noInfiniteSourcing =
      null $ checkWithIncludes  [("lib", "source lib")] "source lib"
  
  prop_canSourceBadSyntax =
      [1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
  
  prop_cantSourceDynamic =
      [1090] == checkWithIncludes [("lib", "")] ". \"$1\""
  
  prop_cantSourceDynamic2 =
      [1090] == checkWithIncludes [("lib", "")] "source ~/foo"
  
  prop_canStripPrefixAndSource =
      null $ checkWithIncludes [("./lib", "")] "source \"$MYDIR/lib\""
  
  prop_canStripPrefixAndSource2 =
      null $ checkWithIncludes [("./utils.sh", "")] "source \"$(dirname \"${BASH_SOURCE[0]}\")/utils.sh\""
  
  prop_canSourceDynamicWhenRedirected =
      null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
  
  prop_canRedirectWithSpaces =
      null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
  
  prop_recursiveAnalysis =
      [2086] == checkRecursive [("lib", "echo $1")] "source lib"
  
  prop_recursiveParsing =
      [1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib"
  
  prop_nonRecursiveAnalysis =
      null $ checkWithIncludes [("lib", "echo $1")] "source lib"
  
  prop_nonRecursiveParsing =
      null $ checkWithIncludes [("lib", "echo \"$10\"")] "source lib"
  
  prop_sourceDirectiveDoesntFollowFile =
      null $ checkWithIncludes
                  [("foo", "source bar"), ("bar", "baz=3")]
                  "#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
  
  prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1"
  prop_filewideAnnotation1 = null $
      check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
  prop_filewideAnnotation2 = null $
      check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
  prop_filewideAnnotation3 = null $
      check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
  prop_filewideAnnotation4 = null $
      check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
  prop_filewideAnnotation5 = null $
      check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
  prop_filewideAnnotation6 = null $
      check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
  prop_filewideAnnotation7 = null $
      check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
  
  prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1"
  prop_filewideAnnotation8 = null $
      check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
  
  prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source'
      3046 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
  
  prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
  
  prop_deducesTypeFromExtension = null result
    where
      result = checkWithSpec [] emptyCheckSpec {
          csFilename = "file.ksh",
          csScript = "(( 3.14 ))"
      }
  
  prop_deducesTypeFromExtension2 = result == [2079]
    where
      result = checkWithSpec [] emptyCheckSpec {
          csFilename = "file.bash",
          csScript = "(( 3.14 ))"
      }
  
  prop_canDisableShebangWarning = null $ result
    where
      result = checkWithSpec [] emptyCheckSpec {
          csFilename = "file.sh",
          csScript = "#shellcheck disable=SC2148\nfoo"
      }
  
  prop_canDisableAllWarnings = result == [2086]
    where
      result = checkWithSpec [] emptyCheckSpec {
          csFilename = "file.sh",
          csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`"
      }
  
  prop_canDisableParseErrors = null $ result
    where
      result = checkWithSpec [] emptyCheckSpec {
          csFilename = "file.sh",
          csScript = "#shellcheck disable=SC1073,SC1072,SC2148\n()"
      }
  
  prop_shExtensionDoesntMatter = result == [2148]
    where
      result = checkWithSpec [] emptyCheckSpec {
          csFilename = "file.sh",
          csScript = "echo 'hello world'"
      }
  
  prop_sourcedFileUsesOriginalShellExtension = result == [2079]
    where
      result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec {
          csFilename = "file.bash",
          csScript = "source file.ksh",
          csCheckSourced = True
      }
  
  prop_canEnableOptionalsWithSpec = result == [2244]
    where
      result = checkWithSpec [] emptyCheckSpec {
          csFilename = "file.sh",
          csScript = "#!/bin/sh\n[ \"$1\" ]",
          csOptionalChecks = ["avoid-nullary-conditions"]
      }
  
  prop_optionIncludes1 =
      -- expect 2086, but not included, so nothing reported
      null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var"
  
  prop_optionIncludes2 =
      -- expect 2086, included, so it is reported
      [2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var"
  
  prop_optionIncludes3 =
      -- expect 2086, no inclusions provided, so it is reported
      [2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var"
  
  prop_optionIncludes4 =
      -- expect 2086 & 2154, only 2154 included, so only that's reported
      [2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
  
  
  prop_readsRcFile = null result
    where
      result = checkWithRc "disable=2086" emptyCheckSpec {
          csScript = "#!/bin/sh\necho $1",
          csIgnoreRC = False
      }
  
  prop_canUseNoRC = result == [2086]
    where
      result = checkWithRc "disable=2086" emptyCheckSpec {
          csScript = "#!/bin/sh\necho $1",
          csIgnoreRC = True
      }
  
  prop_NoRCWontLookAtFile = result == [2086]
    where
      result = checkWithRc (error "Fail") emptyCheckSpec {
          csScript = "#!/bin/sh\necho $1",
          csIgnoreRC = True
      }
  
  prop_brokenRcGetsWarning = result == [1134, 2086]
    where
      result = checkWithRc "rofl" emptyCheckSpec {
          csScript = "#!/bin/sh\necho $1",
          csIgnoreRC = False
      }
  
  prop_canEnableOptionalsWithRc = result == [2244]
    where
      result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec {
          csScript = "#!/bin/sh\n[ \"$1\" ]"
      }
  
  prop_sourcePathRedirectsName = result == [2086]
    where
      f "dir/myscript" _ _ "lib" = return "foo/lib"
      result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
          csScript = "#!/bin/bash\nsource lib",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_sourcePathAddsAnnotation = result == [2086]
    where
      f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib"
      result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
          csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_sourcePathWorksWithSpaces = result == [2086]
    where
      f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"
      result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
          csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_sourcePathRedirectsDirective = result == [2086]
    where
      f "dir/myscript" _ _ "lib" = return "foo/lib"
      f _ _ _ _ = return "/dev/null"
      result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
          csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_rcCanAllowExternalSources = result == [2086]
    where
      f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib"
      f a b c d = error $ show ("Unexpected", a, b, c, d)
      result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
          csScript = "#!/bin/bash\nsource mylib",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_rcCanDenyExternalSources = result == [2086]
    where
      f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib"
      f a b c d = error $ show ("Unexpected", a, b, c, d)
      result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
          csScript = "#!/bin/bash\nsource mylib",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_rcCanLeaveExternalSourcesUnspecified = result == [2086]
    where
      f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib"
      f a b c d = error $ show ("Unexpected", a, b, c, d)
      result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
          csScript = "#!/bin/bash\nsource mylib",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_fileCanDisableExternalSources = result == [2006, 2086]
    where
      f "dir/myscript" (Just True) _ "withExternal" = return "withExternal"
      f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal"
      f a b c d = error $ show ("Unexpected", a, b, c, d)
      result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec {
          csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_fileCannotEnableExternalSources = result == [1144]
    where
      f "dir/myscript" Nothing _ "foo" = return "foo"
      f a b c d = error $ show ("Unexpected", a, b, c, d)
      result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec {
          csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_fileCannotEnableExternalSources2 = result == [1144]
    where
      f "dir/myscript" (Just False) _ "foo" = return "foo"
      f a b c d = error $ show ("Unexpected", a, b, c, d)
      result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec {
          csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
          csFilename = "dir/myscript",
          csCheckSourced = True
      }
  
  prop_rcCanSuppressEarlyProblems1 = null result
    where
      result = checkWithRc "disable=1071" emptyCheckSpec {
          csScript = "#!/bin/zsh\necho $1"
      }
  
  prop_rcCanSuppressEarlyProblems2 = null result
    where
      result = checkWithRc "disable=1104" emptyCheckSpec {
          csScript = "!/bin/bash\necho 'hello world'"
      }
  
  prop_sourceWithHereDocWorks = null result
    where
      result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
  
  prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
    where
      result = check "cat << eof"
  
  prop_hereDocsWillHaveParsedIndices = null result
    where
      result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
  
  prop_rcCanSuppressDfa = null result
    where
      result = checkWithRc "extended-analysis=false" emptyCheckSpec {
          csScript = "#!/bin/sh\nexit; foo;"
      }
  
  prop_fileCanSuppressDfa = null $ traceShowId result
    where
      result = checkWithRc "" emptyCheckSpec {
          csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
      }
  
  prop_fileWinsWhenSuppressingDfa1 = null result
    where
      result = checkWithRc "extended-analysis=true" emptyCheckSpec {
          csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
      }
  
  prop_fileWinsWhenSuppressingDfa2 = result == [2317]
    where
      result = checkWithRc "extended-analysis=false" emptyCheckSpec {
          csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
      }
  
  prop_flagWinsWhenSuppressingDfa1 = result == [2317]
    where
      result = checkWithRc "extended-analysis=false" emptyCheckSpec {
          csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
          csExtendedAnalysis = Just True
      }
  
  prop_flagWinsWhenSuppressingDfa2 = null result
    where
      result = checkWithRc "extended-analysis=true" emptyCheckSpec {
          csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
          csExtendedAnalysis = Just False
      }
  
  return []
  runTests = $quickCheckAll
  