diff --git a/CHANGELOG.md b/CHANGELOG.md index c363eb5..5d8b01f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - SC2321: Suggest removing $((..)) in array[$((idx))]=val - SC2322: Suggest collapsing double parentheses in arithmetic contexts - SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val +- support Static Analysis Results Interchange Format (SARIF) output ### Fixed - SC2086: Now uses DFA to make more accurate predictions about values diff --git a/ShellCheck.cabal b/ShellCheck.cabal index b22b5c8..8ac7285 100644 --- a/ShellCheck.cabal +++ b/ShellCheck.cabal @@ -83,6 +83,7 @@ library ShellCheck.Formatter.GCC ShellCheck.Formatter.JSON ShellCheck.Formatter.JSON1 + ShellCheck.Formatter.SARIF ShellCheck.Formatter.TTY ShellCheck.Formatter.Quiet ShellCheck.Interface diff --git a/shellcheck.1.md b/shellcheck.1.md index c345a2b..d30f078 100644 --- a/shellcheck.1.md +++ b/shellcheck.1.md @@ -197,6 +197,10 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts. : This is a legacy version of the **json1** format. It's a raw array of comments, and all offsets have a tab stop of 8. +**sarif** + +: Static Analysis Results Interchange Format (SARIF) output. + **quiet** : Suppress all normal output. Exit with zero if no issues are found, diff --git a/shellcheck.hs b/shellcheck.hs index a525251..60fe144 100644 --- a/shellcheck.hs +++ b/shellcheck.hs @@ -29,6 +29,7 @@ import qualified ShellCheck.Formatter.Diff import qualified ShellCheck.Formatter.GCC import qualified ShellCheck.Formatter.JSON import qualified ShellCheck.Formatter.JSON1 +import qualified ShellCheck.Formatter.SARIF import qualified ShellCheck.Formatter.TTY import qualified ShellCheck.Formatter.Quiet @@ -146,6 +147,7 @@ formats options = Map.fromList [ ("gcc", ShellCheck.Formatter.GCC.format), ("json", ShellCheck.Formatter.JSON.format), ("json1", ShellCheck.Formatter.JSON1.format), + ("sarif", ShellCheck.Formatter.SARIF.format), ("tty", ShellCheck.Formatter.TTY.format options), ("quiet", ShellCheck.Formatter.Quiet.format options) ] @@ -576,7 +578,7 @@ printVersion = do putStrLn "ShellCheck - shell script analysis tool" putStrLn $ "version: " ++ shellcheckVersion putStrLn "license: GNU General Public License, version 3" - putStrLn "website: https://www.shellcheck.net" + putStrLn $ "website: " ++ shellcheckWebsite printOptional = do mapM f list diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs index 4090922..bf873ef 100644 --- a/src/ShellCheck/Data.hs +++ b/src/ShellCheck/Data.hs @@ -22,7 +22,7 @@ Use: import Paths_ShellCheck (version) shellcheckVersion = showVersion version -- VERSIONSTRING - +shellcheckWebsite = "https://www.shellcheck.net" internalVariables = [ -- Generic diff --git a/src/ShellCheck/Formatter/SARIF.hs b/src/ShellCheck/Formatter/SARIF.hs new file mode 100644 index 0000000..74b2149 --- /dev/null +++ b/src/ShellCheck/Formatter/SARIF.hs @@ -0,0 +1,140 @@ +{-# LANGUAGE OverloadedStrings #-} +{- + Copyright 2012-2019 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 . +-} +module ShellCheck.Formatter.SARIF (format) where + +import ShellCheck.Data +import ShellCheck.Interface +import ShellCheck.Formatter.Format + +import Control.DeepSeq +import Data.Aeson +import Data.IORef +import Data.Monoid +import GHC.Exts +import System.IO +import qualified Data.ByteString.Lazy.Char8 as BL + +format :: IO Formatter +format = do + ref <- newIORef [] + return Formatter { + header = return (), + onResult = collectResult ref, + onFailure = outputError, + footer = finish ref + } + +data SarifOutput = SarifOutput { + comments :: [PositionedComment] + } + +instance ToJSON SarifOutput where + toJSON result = object [ + "version" .= ("2.1.0" :: String), + "$schema" .= ("http://json.schemastore.org/sarif-2.1.0" :: String), + "runs" .= [object [ + "tool" .= object [ + "driver" .= object [ + "name" .= ("ShellCheck" :: String), + "semanticVersion" .= shellcheckVersion, + "informationUri" .= shellcheckWebsite + ] + ], + "results" .= comments result + ]] + ] + +instance ToJSON Replacement where + toJSON replacement = + let start = repStartPos replacement + end = repEndPos replacement in + object [ + "artifactLocation" .= object [ + "uri" .= posFile start + ], + "replacements" .= [object [ + "deletedRegion" .= object [ + "startLine" .= posLine start, + "startColumn" .= posColumn start, + "endLine" .= posLine end, + "endColumn" .= posColumn end + ], + "insertedContent" .= object [ + "text" .= repString replacement + ] + ]] + ] + +instance ToJSON PositionedComment where + toJSON comment = + let start = pcStartPos comment + end = pcEndPos comment + c = pcComment comment + replacements = maybe [] fixReplacements (pcFix comment) in + object [ + "ruleId" .= code (cCode c), + "level" .= level (cSeverity c), + "message" .= object [ + "text" .= cMessage c + ], + "locations" .= [object [ + "physicalLocation" .= object [ + "artifactLocation" .= object [ + "uri" .= posFile start + ], + "region" .= object [ + "startLine" .= posLine start, + "startColumn" .= posColumn start, + "endLine" .= posLine end, + "endColumn" .= posColumn end + ] + ] + ]], + "fixes" .= [object [ + "artifactChanges" .= replacements + ] | not (null replacements)] + ] + +code num = "SC" ++ show num + +level severity = case severity of + ErrorC -> "error" :: String + WarningC -> "warning" + InfoC -> "note" + StyleC -> "none" + +outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg + +collectResult ref cr sys = mapM_ f groups + where + comments = crComments cr + groups = groupWith sourceFile comments + f :: [PositionedComment] -> IO () + f group = do + let filename = sourceFile (head group) + result <- siReadFile sys (Just True) filename + let contents = either (const "") id result + let comments' = makeNonVirtual comments contents + deepseq comments' $ modifyIORef ref (\x -> comments' ++ x) + +finish ref = do + list <- readIORef ref + BL.putStrLn $ encode $ SarifOutput { comments = list }