diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 055b99c..ed255ce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,8 +29,7 @@ build-job: # This job runs in the build stage, which runs first. script: - make release artifacts: - paths: - - build/dist/*.tar.gz + - build/dist/*.tar.gz unit-test-job: # This job runs in the test stage. stage: test # It only starts when the job in the build stage completes successfully. @@ -39,7 +38,7 @@ unit-test-job: # This job runs in the test stage. - linux script: - echo "Running unit tests... This will take about 60 seconds." - - sleep 1 + - sleep 60 - echo "Code coverage is 90%" lint-test-job: # This job also runs in the test stage. diff --git a/Makefile b/Makefile index b656f16..970b7fd 100644 --- a/Makefile +++ b/Makefile @@ -1,32 +1,27 @@ -.PHONY: default all install watch clean build release distclean lint doc +.PHONY: default all install watch clean build release distclean lint default: all all: build +build: builddir build/bin/annotator + GHCFLAGS = -iapp -isrc -Wall -O2 -outputdir build/.obj BUILDDIR = build DESTDIR = ~/.local BINDIR = ${DESTDIR}/bin DOCDIR = ${DESTDIR}/share/doc/annotator -build: builddir | $(BUILDDIR)/bin/annotator doc - builddir: - mkdir -p $(BUILDDIR)/{bin,.obj,ci,dist,doc} - -doc: builddir | $(BUILDDIR)/doc/README.md + mkdir -p $(BUILDDIR)/bin $(BUILDDIR)/.obj $(BUILDDIR)/ci $(BUILDDIR)/dist $(BUILDDIR)/bin/annotator: app/Main.hs src/*.hs ghc --make $(GHCFLAGS) $< -o $@ -$(BUILDDIR)/doc/README.md: README.org - pandoc -t gfm $< -o $@ - watch: @git ls-files src app Makefile | entr make -s build install -lint: builddir - hlint lint --git --cc --no-exit-code | tr -d '\0' | jq -s . > $(BUILDDIR)/ci/hlint.report.json +lint: + @-hlint -g --cc > $(BUILDDIR)/ci/hlint.report.json clean: @rm -rf $(BUILDDIR) @@ -36,10 +31,9 @@ clean: distclean: clean @rm -rf dist -install: build - @install -m 0755 -D -t ${BINDIR} $(BUILDDIR)/bin/annotator - @install -m 0644 -D -t ${DOCDIR} README.org - @install -m 0644 -D -t ${DOCDIR} $(BUILDDIR)/doc/README.md +install: $(BUILDDIR)/bin/annotator + @install -m 0755 -D -t ${BINDIR} $< + @install -m 0644 -D -t ${DOCDIR} README.md release: script/make-release @$< diff --git a/README.md b/README.md new file mode 100644 index 0000000..bae8d08 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Annotator - an interactive TM2/TM3 annotation tool + +This tool lets you interactively annotate your code given a =defects.err= file. + +## Installation + +Get the most recent release, unpack it and add the binary to your PATH. + +## How to use it + +After a /Coverity™/ run, you end up with a =defects.err= and would like to +annotate your code accordingly. In order to do so, just execute the =annotator=: + +``` +annotator +``` + +This will by default use the =defects.err= file in the current directory and +scan it for violations. It will then ask you what to do with each violation - by +default only /Newest/ violations will be handled, but this can be overridden +with a command-line switch. + +After all violations have been treated, you'll end up with a bunch of =*.fix= +files next to each source file - those are the annotated source files - you may +run diff on them to check if they look fine or just move them over your original +source file. + +### Possible annotations + +The annotator is able to generate three kinds of annotations: + + - *Intentional* - by pressing `i`, this will annotate with `coverity[rule] ` + - *False-Positive* - by pressing `f`, this will annotate with `coverity[rule : FALSE] ` + - *Todo* - by pressing `t`, this will annotate with a FIXME marker + +## Advanced usage scenarios + +The current annotator supports the following command-line arguments: + +``` +$ annotator --help +Usage: annotator [OPTIONS] files... + +A tool to semi-automatically add Coverity source-code annotations based on found defects. + + + -v --verbose be more verbose, pass multiple times to increase verbosity + -i --inplace replace source-file after inserting annotations + -V --version show full version information + --short-version show just the version number + -h --help show usage information + -a --all handle all defects not just Newest + -C[NUM] --context[=NUM] specify how much context should be shown around a violation + -t[STRING] --todo-marker[=STRING] override the default TODO marker with a custom string + -A FILE --annotations=FILE load automatic annotation rules +``` + +### In-place annotations + +The `annotator` allows to annotate in-place, i.e. it will automatically rename +the `.fix` file after you are done with all violations within that file. This +can be achieved by passing `-i` or `--inplace`. + +### Process all violations + +By default the annotator will only handle *Newest* violations and not those that +are already contained within the Coverity database for some reason. However, +it's still possible to process all found violations by passing `-a` or `--all` +on the command-line. + +### Insert annotations automatically + +In case you have violations that always result in the same annotation over and +over again, you can supply one or more files that contain automatic decisions. + +Each line may be one of the following: + +``` +Intentional (Rule "autosar_cpp14_a18_9_1") "reason why it's intentional" +FalsePositive (Rule "autosar_cpp14_a18_9_1") "reason why it's a false-positive" +ToDo (Rule "autosar_cpp14_a18_9_1") +``` + +Lines may be disabled by prefixing them with `#` or `--`. diff --git a/README.org b/README.org index 591bada..3e55f92 100644 --- a/README.org +++ b/README.org @@ -1,54 +1,14 @@ -#+title: Annotator - an interactive Coverity annotation tool - -* Abstract - -This tool lets you interactively annotate your code given a =defects.err= file. The file should contain file and line information along with the Coverity rule that was violated. An example may look like this: - -#+begin_example csv -main.cpp:162:INFO: Newest, autosar_cpp14_a20_8_6_violation: Object "std::unique_ptr >(read_configuration(uri))" is not constructed using "std::make_shared". -#+end_example +#+title: Readme * Installation -Get the most recent release, unpack it and add the binary to your PATH. - * Usage -After a /Coverity™/ run, you end up with a =defects.err= and would like to -annotate your code accordingly. In order to do so, just execute the =annotator=: - -#+begin_src sh -annotator -#+end_src - -This will by default use the =defects.err= file in the current directory and -scan it for violations. It will then ask you what to do with each violation - by -default only /Newest/ violations will be handled, but this can be overridden -with a command-line switch. - -After all violations have been treated, you'll end up with a bunch of =*.fix= -files next to each source file - those are the annotated source files - you may -run diff on them to check if they look fine or just move them over your original -source file. - -** Possible annotations - -The annotator is able to generate three kinds of annotations: - -- Intentional :: by pressing =i=, this will annotate with =coverity[rule] = -- FalsePositive :: by pressing =f=, this will annotate with =coverity[rule : FALSE] = -- Todo :: by pressing =t=, this will annotate with a TODO marker - -* Advanced usage scenarios - -The current annotator supports the following command-line arguments: - -#+name: annotator-help -#+begin_src sh :results output :exports both +#+begin_src sh :results output annotator --help #+end_src -#+RESULTS: annotator-help +#+RESULTS: #+begin_example Usage: annotator [OPTIONS] files... @@ -72,25 +32,3 @@ A tool to semi-automatically add Coverity source-code annotations based on found # a comment -- another comment #+end_example - -** In-place annotations - -The =annotator= allows to annotate in-place, i.e. it will automatically rename the =.fix= file after you are done with all violations within that file. This can be achieved by passing =-i= or =--inplace=. - -** Process all violations - -By default the annotator will only handle *Newest* violations and not those that are already contained within the Coverity database for some reason. However, it's still possible to process all found violations by passing =-a= or =--all= on the command-line. - -** Insert annotations automatically - -In case you have violations that always result in the same annotation over and over again, you can supply one or more files that contain automatic decisions. - -Each line may be one of the following: - -#+begin_example haskell -Intentional (Rule "autosar_cpp14_a18_9_1") "reason why it's intentional" -FalsePositive (Rule "autosar_cpp14_a18_9_1") "reason why it's a false-positive" -ToDo (Rule "autosar_cpp14_a18_9_1") -#+end_example - -Lines may be disabled by prefixing them with =#= or =--=. diff --git a/example/include/process/definition_receiver.hpp b/example/include/process/definition_receiver.hpp index 0dc521c..a4ee945 100644 --- a/example/include/process/definition_receiver.hpp +++ b/example/include/process/definition_receiver.hpp @@ -4,7 +4,7 @@ #include #include -#include "process_definition.hpp" +#include "process_definition.hpp" // TODO generate to namespace process::controller { class DefinitionListener; diff --git a/example/include/process/predicate.hpp b/example/include/process/predicate.hpp index ae96cee..eefc8df 100644 --- a/example/include/process/predicate.hpp +++ b/example/include/process/predicate.hpp @@ -1,7 +1,7 @@ #ifndef PROCESS_CONTROLLER_PREDICATE_HPP #define PROCESS_CONTROLLER_PREDICATE_HPP -#include "process_definition.hpp" +#include "process_definition.hpp" // TODO: generate to /idl/process/process_definition.hpp #include #include @@ -62,6 +62,8 @@ namespace predicate { /** * returns a function that checks whether the node of a ProcessDefinition matches a given pattern + * + * TODO implement more sophisticated glob matching, we currently support '*' and exact match */ [[nodiscard]] inline std::function node_matches(const std::string& pattern) noexcept { diff --git a/src/Annotator.hs b/src/Annotator.hs index eacc283..90191c0 100644 --- a/src/Annotator.hs +++ b/src/Annotator.hs @@ -1,6 +1,7 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE StrictData #-} +{-# LANGUAGE BangPatterns #-} module Annotator (defaultMain) where @@ -16,7 +17,7 @@ import Control.Monad.IO.Class import Control.Monad.Trans.Reader import Control.Exception (evaluate) import Data.Function (on) -import Data.List (delete, foldl', sortOn, nub) +import Data.List (delete, intercalate, foldl', sortBy, nub) import Data.Char (isSpace) import Data.Either (rights) import Data.Ord @@ -30,7 +31,7 @@ import Annotator.Rule import Annotator.Annotation version :: Vsn.Version -version = Vsn.makeVersion [0,0,5,2] +version = Vsn.makeVersion [0,0,5,1] type App a = ReaderT Options IO a @@ -103,7 +104,7 @@ options = (ReqArg (\f opts -> opts { todoMarker = f }) "STRING") "override the default TODO marker with a custom string" , Option ['A'] ["annotations"] - (ReqArg (\f opts -> opts { annotationFiles = f : annotationFiles opts }) "FILE") + (ReqArg (\f opts -> opts { annotationFiles = (nub $ annotationFiles opts ++ [f]) }) "FILE") (unlines [ "load automatic annotation rules" , " some examples:" , " " <> show (Intentional (Rule "rule_1") "some reason") @@ -123,9 +124,7 @@ readMaybe s = case reads s of parseOptions :: [String] -> IO (Options, [String]) parseOptions argv = case getOpt Permute options argv of - (o, n, []) -> do - let o' = foldl' (flip id) defaultOptions o - pure (o'{annotationFiles = reverse $ nub $ annotationFiles o'}, n) + (o, n, []) -> pure (foldl' (flip id) defaultOptions o, n) (_, _, errs) -> ioError (userError (concat errs ++ usageInfo header options)) where header = "Usage: annotator [OPTIONS] files..." @@ -146,18 +145,18 @@ defaultMain = do Just FullVersion -> do putStr $ unlines [ - "Annotator v" <> Vsn.showVersion version + "Annotator v" <> (Vsn.showVersion version) , "Copyright (c) 2022 Alexander Kobjolke " ] - exitSuccess + exitWith ExitSuccess Just ShortVersion -> do putStrLn $ Vsn.showVersion version - exitSuccess + exitWith ExitSuccess Nothing -> pure () when (showHelp opts) $ do putStr $ usageInfo header options - exitSuccess + exitWith ExitSuccess automaticAnnotations <- rights . concat <$> (filterM fileExist (annotationFiles opts) >>= mapM fromFile) @@ -166,8 +165,9 @@ defaultMain = do runReaderT (genericMain fn) opts' where - header = unlines - [ "Usage: annotator [OPTIONS] files..." + header = unlines $ + [ + "Usage: annotator [OPTIONS] files..." , "" , "A tool to semi-automatically add Coverity source-code annotations based on found defects." , "" @@ -177,11 +177,11 @@ data Age = Old | New | Newest deriving (Show, Eq) data Violation = Violation { - filename :: FilePath, - line :: Int, - age :: Age, - rule :: Rule, - description :: String + filename :: !FilePath, + line :: !Int, + age :: !Age, + rule :: !Rule, + description :: !String } deriving (Show, Eq) @@ -190,7 +190,7 @@ split _ _ [] = [] split n p xs | n > 0 = case break p xs of (match, []) -> [match] - (match, _ : rest) -> match : split (n-1) p rest + (match, (_ : rest)) -> match : split (n-1) p rest | otherwise = [xs] parseViolations :: String -> [Violation] @@ -208,7 +208,7 @@ parseViolation s = case split 4 (== ':') s of where violation = Violation file (read line) a (Rule (removeSuffix "_violation" r)) (dropWhile isSpace $ concat desc) (_:age':r:_) = split 2 (== ' ') rule' - a = case delete ',' age' of + a = case (delete ',' age') of "Newest" -> Newest "New" -> New _ -> Old @@ -230,7 +230,7 @@ genericMain file = do verbose Chatty $ "all violations: " <> show sortedViolations forM_ groupedViolations handleViolations - Nothing -> + Nothing -> do liftIO $ hPutStrLn stderr "Defects file is empty" where @@ -249,10 +249,10 @@ handleViolations violations = do let fname = filename $ NE.head violations fname' = fname <> ".fix" todo = todoMarker opts - header' = "Processing " <> show (NE.length violations) <> " violation(s) in file " <> fname + header' = "Processing " <> (show $ NE.length violations) <> " violation(s) in file " <> fname header = unlines [ header' - , replicate (length header') '=' + , take (length header') $ repeat '=' , "" ] @@ -268,15 +268,15 @@ handleViolations violations = do let numberedContent = zip [1..] . lines $ content - annotations <- catMaybes . NE.toList <$> mapM (handleViolation content) violations + annotations <- (catMaybes . NE.toList) <$> mapM (handleViolation content) violations - let annotatedLines = sortOn fst $ fmap (annotationToLine todo) annotations + let annotatedLines = sortBy (comparing fst) $ fmap (annotationToLine todo) annotations newContent = unlines . map snd $ mergeLines annotatedLines numberedContent liftIO $ writeFile fname' newContent liftIO $ when (inplace opts) $ rename fname' fname - else + else do verbose Low $ "skipping non-existent file " <> fname where annotationToLine :: String -> AnnotatedViolation -> (Int, String) @@ -296,17 +296,24 @@ data UserChoice = Abort -- | let the user decide what to do with a violation getUserChoice :: Violation -> App UserChoice -getUserChoice Violation{..} = liftIO queryUser +getUserChoice Violation{..} = do + liftIO $ queryUser where queryUser = do - putStr "> What shall we do [s/t/i/f/q/?]: " + putStr $ "> What shall we do [s/t/i/f/q/?]: " hFlush stdout c <- getChar putStrLn "" case c of - 's' -> pure Skip - 't' -> pure $ Annotate (ToDo rule) - 'i' -> Annotate . Intentional rule <$> getExcuse - 'f' -> Annotate . FalsePositive rule <$> getExcuse + 's' -> do + pure Skip + 't' -> do + pure $ Annotate (ToDo rule) + 'i' -> do + excuse <- getExcuse + pure $ Annotate (Intentional rule excuse) + 'f' -> do + excuse <- getExcuse + pure $ Annotate (FalsePositive rule excuse) 'q' -> pure Abort '?' -> do putStrLn $ unlines [ "t - add TODO marker to fix this issue" @@ -321,7 +328,7 @@ getUserChoice Violation{..} = liftIO queryUser _ -> queryUser getExcuse = do - putStr "What's your excuse? " + putStr $ "What's your excuse? " hFlush stdout mode <- hGetBuffering stdin hSetBuffering stdin LineBuffering @@ -342,7 +349,7 @@ type NumberedLine = (Int, String) mergeLines :: [NumberedLine] -> [NumberedLine] -> [NumberedLine] mergeLines [] r = r mergeLines l [] = l -mergeLines lhs@(left@(nl,_):ls) rhs@(right@(nr,_):rs) +mergeLines !lhs@(left@(nl,_):ls) !rhs@(right@(nr,_):rs) | nl <= nr = left : mergeLines ls rhs | otherwise = right : mergeLines lhs rs @@ -372,21 +379,21 @@ handleViolation content v@Violation{..} = do -- print some context liftIO $ forM_ context (\(n, code) -> do let marker = ">>>>" - when (n == line) $ putStrLn (unwords [ marker - , show age - , "violation of rule" - , show rule - , "in line" - , show line <> ":" - , description - ]) - putStrLn code) + when (n == line) $ putStrLn (intercalate " " [ marker + , show age + , "violation of rule" + , show rule + , "in line" + , show line <> ":" + , description + ]) + putStrLn (code)) if batchMode opts - then pure Nothing + then pure $ Nothing else do choice <- getUserChoice v case choice of - Abort -> liftIO exitSuccess + Abort -> liftIO $ exitSuccess Annotate annotation -> pure $ Just (AnnotatedViolation v annotation indent) Skip -> pure Nothing Help -> handleViolation content v diff --git a/src/Annotator/Annotation.hs b/src/Annotator/Annotation.hs index ae092f5..5f9e6a8 100644 --- a/src/Annotator/Annotation.hs +++ b/src/Annotator/Annotation.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} module Annotator.Annotation (Annotation(..), fromFile) where import Data.Char (isSpace) diff --git a/src/Annotator/Util.hs b/src/Annotator/Util.hs index 2ce4b2c..4fb234f 100644 --- a/src/Annotator/Util.hs +++ b/src/Annotator/Util.hs @@ -1,4 +1,4 @@ module Annotator.Util where anyp :: [a -> Bool] -> a -> Bool -anyp preds x = any ($ x) preds +anyp preds x = or (map ($ x) preds)