From 747b55239d3bf21071b16b2dd8c97a9b5a4f9043 Mon Sep 17 00:00:00 2001 From: Marlon Moser Date: Thu, 19 Jun 2025 11:01:50 +0200 Subject: [PATCH] feat(formatter): add codeclimate formatter --- CHANGELOG.md | 1 + ShellCheck.cabal | 1 + shellcheck.hs | 2 + src/ShellCheck/Formatter/Codeclimate.hs | 139 ++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 src/ShellCheck/Formatter/Codeclimate.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc8a79..2bbc648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash. - SC3062: Warn about bashism `[ -o opt ]`. - Precompiled binaries for Linux riscv64 (linux.riscv64) +- Codeclimate: New Codeclimate formatter for GitLab Pipelines. ### Changed - SC2002 about Useless Use Of Cat is now disabled by default. It can be re-enabled with `--enable=useless-use-of-cat` or equivalent directive. diff --git a/ShellCheck.cabal b/ShellCheck.cabal index 68c32d9..a887f7b 100644 --- a/ShellCheck.cabal +++ b/ShellCheck.cabal @@ -89,6 +89,7 @@ library ShellCheck.Formatter.GCC ShellCheck.Formatter.JSON ShellCheck.Formatter.JSON1 + ShellCheck.Formatter.Codeclimate ShellCheck.Formatter.TTY ShellCheck.Formatter.Quiet ShellCheck.Interface diff --git a/shellcheck.hs b/shellcheck.hs index def3654..3b84325 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.Codeclimate import qualified ShellCheck.Formatter.TTY import qualified ShellCheck.Formatter.Quiet @@ -155,6 +156,7 @@ formats options = Map.fromList [ ("gcc", ShellCheck.Formatter.GCC.format), ("json", ShellCheck.Formatter.JSON.format), ("json1", ShellCheck.Formatter.JSON1.format), + ("codeclimate", ShellCheck.Formatter.Codeclimate.format), ("tty", ShellCheck.Formatter.TTY.format options), ("quiet", ShellCheck.Formatter.Quiet.format options) ] diff --git a/src/ShellCheck/Formatter/Codeclimate.hs b/src/ShellCheck/Formatter/Codeclimate.hs new file mode 100644 index 0000000..07a1e01 --- /dev/null +++ b/src/ShellCheck/Formatter/Codeclimate.hs @@ -0,0 +1,139 @@ +{-# LANGUAGE OverloadedStrings #-} +module ShellCheck.Formatter.Codeclimate (format) where + +import ShellCheck.Interface +import ShellCheck.Formatter.Format + +import Control.DeepSeq (deepseq) +import Data.Aeson +import Data.IORef +import System.IO (hPutStrLn, stderr) +import qualified Data.ByteString.Lazy.Char8 as BL +import qualified Data.List.NonEmpty as NE +import qualified Data.ByteString.Char8 as BS + +format :: IO Formatter +format = do + ref <- newIORef [] + return Formatter + { header = return () + , onResult = collectResult ref + , onFailure = outputError + , footer = finish ref + } + +data CCIssue = CCIssue + { description :: String + , check_name :: String + , fingerprint :: String + , severity :: String + , location :: CCLocation + } + +data CCLocation = CCLocation + { path :: String + , positions :: CCPositions + } + +data CCPositions = CCPositions + { begin :: CCPosition + , end :: CCPosition + } + +data CCPosition = CCPosition + { line :: Integer + , column :: Integer + } + +-- ToJSON instances +instance ToJSON CCIssue where + toJSON issue = object + [ "type" .= ("issue" :: String) + , "description" .= description issue + , "check_name" .= check_name issue + , "fingerprint" .= fingerprint issue + , "severity" .= severity issue + , "location" .= location issue + ] + toEncoding issue = pairs + ( "type" .= ("issue" :: String) + <> "description" .= description issue + <> "check_name" .= check_name issue + <> "fingerprint" .= fingerprint issue + <> "severity" .= severity issue + <> "location" .= location issue + ) + +instance ToJSON CCLocation where + toJSON loc = object + [ "path" .= path loc + , "positions" .= positions loc + ] + +instance ToJSON CCPositions where + toJSON pos = object + [ "begin" .= begin pos + , "end" .= end pos + ] + +instance ToJSON CCPosition where + toJSON p = object + [ "line" .= line p + , "column" .= column p + ] + +-- Mapping ShellCheck PositionedComment -> CCIssue +toCCIssue :: PositionedComment -> CCIssue +toCCIssue pc = + let start = pcStartPos pc + endPos = pcEndPos pc + filePath = posFile start + lineNum = posLine start + endLineNum = posLine endPos + columnNum = posColumn start + endColumnNum = posColumn endPos + c = pcComment pc + codeNum = cCode c + msg = cMessage c + desc = msg + checkName = "SC" ++ show codeNum + fingerprint = filePath ++ ":" ++ show lineNum ++ ":" ++ show codeNum + sevText = severityText pc + severityCC = mapSeverity sevText + in CCIssue + { description = desc + , check_name = checkName + , fingerprint = fingerprint + , severity = severityCC + , location = CCLocation filePath (CCPositions (CCPosition lineNum columnNum) (CCPosition endLineNum endColumnNum)) + } + +-- ShellCheck severity levels to Code Climate levels +mapSeverity :: String -> String +mapSeverity "error" = "critical" +mapSeverity "warning" = "major" +mapSeverity "info" = "minor" +mapSeverity "style" = "info" +mapSeverity _ = "minor" + +outputError :: FilePath -> String -> IO () +outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg + +collectResult ref cr sys = mapM_ f groups + where + commentsAll = crComments cr + groups = NE.groupWith sourceFile commentsAll + f :: NE.NonEmpty PositionedComment -> IO () + f group = do + let filename = sourceFile (NE.head group) + result <- siReadFile sys (Just True) filename + let contents = either (const "") id result + let comments' = makeNonVirtual commentsAll contents + deepseq comments' $ modifyIORef ref (\x -> comments' ++ x) + +finish :: IORef [PositionedComment] -> IO () +finish ref = do + pcs <- readIORef ref + let issues = map toCCIssue pcs + BL.putStrLn $ encode issues +