489 lines
16 KiB
Haskell
489 lines
16 KiB
Haskell
{-
|
|
Copyright 2012-2020 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.Interface
|
|
import ShellCheck.Parser
|
|
import ShellCheck.Analyzer
|
|
|
|
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,
|
|
asOptionalChecks = 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_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_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_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
|
|
}
|
|
|
|
|
|
return []
|
|
runTests = $quickCheckAll
|