/src/AndroidLintSummary.hs

https://github.com/passy/android-lint-summary · Haskell · 253 lines · 203 code · 41 blank · 9 comment · 11 complexity · 24aa54acfab0cfd72885cff1e54b6a11 MD5 · raw file

  1. {-# LANGUAGE Arrows #-}
  2. {-# LANGUAGE ExistentialQuantification #-}
  3. {-# LANGUAGE NoImplicitPrelude #-}
  4. {-# LANGUAGE OverloadedStrings #-}
  5. {-# LANGUAGE TemplateHaskell #-}
  6. -- | Parsers and pretty printers for the `lint-results.xml` file format.
  7. module AndroidLintSummary (
  8. supportedLintFormatVersion
  9. , AppEnv(..)
  10. , AppOpts(..)
  11. , LintSeverity(..)
  12. , LintFormatter(..)
  13. , LintLocation()
  14. , LintIssue()
  15. , Verbosity(..)
  16. , readLintIssues
  17. , openXMLFile
  18. , indentWrap
  19. , formatLintIssues
  20. , filename
  21. , line
  22. , column
  23. , severity
  24. , summary
  25. , priority
  26. , explanation
  27. , location
  28. , formatter
  29. , glob
  30. , targets
  31. , verbose
  32. ) where
  33. import BasicPrelude hiding (fromString)
  34. import Rainbow
  35. import Text.XML.HXT.Core
  36. import Control.Lens hiding (deep)
  37. import Control.Monad.Reader (Reader (), ask)
  38. import Data.Default (Default (), def)
  39. import Data.Stringable (Stringable (..))
  40. import System.FilePath.GlobPattern (GlobPattern)
  41. import System.IO (Handle (), IOMode (ReadMode),
  42. openFile, stdin)
  43. import qualified Data.Text as T
  44. import qualified Data.Text.IO as TIO
  45. import qualified System.Console.Terminal.Size as Terminal
  46. supportedLintFormatVersion :: String
  47. supportedLintFormatVersion = "4"
  48. defaultLintResultsGlob :: GlobPattern
  49. defaultLintResultsGlob = "**/build/outputs/lint-results.xml"
  50. data LintSeverity = FatalSeverity
  51. | ErrorSeverity
  52. | WarningSeverity
  53. | InformationalSeverity
  54. deriving (Eq, Ord, Show, Bounded, Enum)
  55. data LintLocation = LintLocation { _filename :: FilePath
  56. , _line :: Maybe Int
  57. , _column :: Maybe Int
  58. }
  59. deriving (Eq, Show)
  60. makeLenses ''LintLocation
  61. data LintIssue = LintIssue { _severity :: LintSeverity
  62. , _summary :: T.Text
  63. , _priority :: Int
  64. , _explanation :: T.Text
  65. , _location :: LintLocation
  66. }
  67. deriving (Eq, Show)
  68. makeLenses ''LintIssue
  69. data LintFormatter =
  70. NullLintFormatter -- ^ A formatter that doesn't output
  71. -- anything.
  72. | SimpleLintFormatter -- ^ A formatter that displays the errors
  73. -- in descending errors with simple color
  74. -- coding.
  75. deriving (Eq, Show, Bounded, Enum)
  76. data Verbosity = Normal | Verbose
  77. deriving (Show, Eq)
  78. data AppOpts = AppOpts { _targets :: Maybe [FilePath]
  79. , _glob :: GlobPattern
  80. , _formatter :: LintFormatter
  81. , _verbose :: Verbosity
  82. }
  83. deriving (Show, Eq)
  84. makeLenses ''AppOpts
  85. data AppEnv = AppEnv { _opts :: AppOpts
  86. , _terminalSize :: Maybe (Terminal.Window Int)
  87. }
  88. makeLenses ''AppEnv
  89. instance Default AppOpts where
  90. def = AppOpts { _targets = mempty
  91. , _glob = defaultLintResultsGlob
  92. , _formatter = SimpleLintFormatter
  93. , _verbose = Normal
  94. }
  95. instance Stringable LintSeverity where
  96. toString = formatSeverity
  97. fromString s
  98. | s == "Fatal" = FatalSeverity
  99. | s == "Error" = ErrorSeverity
  100. | s == "Warning" = WarningSeverity
  101. | s == "Information" = InformationalSeverity
  102. | otherwise = error $ "Invalid severity " <> s
  103. length _ = 0
  104. instance Stringable LintFormatter where
  105. toString NullLintFormatter = "null"
  106. toString SimpleLintFormatter = "simple"
  107. fromString s
  108. | s == "null" = NullLintFormatter
  109. | s == "simple" = SimpleLintFormatter
  110. | otherwise = error "Invalid LintFormatter specification"
  111. length _ = 0
  112. formatSeverity :: LintSeverity -> String
  113. formatSeverity FatalSeverity = "Fatal"
  114. formatSeverity ErrorSeverity = "Error"
  115. formatSeverity WarningSeverity = "Warning"
  116. formatSeverity InformationalSeverity = "Information"
  117. colorSeverity :: LintSeverity -> Chunk a -> Chunk a
  118. colorSeverity FatalSeverity a = a & fore red & bold
  119. colorSeverity ErrorSeverity a = a & fore red
  120. colorSeverity WarningSeverity a = a & fore yellow
  121. colorSeverity InformationalSeverity a = a & fore white
  122. formatLintIssues :: LintFormatter -> [LintIssue] -> Reader AppEnv [Chunk T.Text]
  123. formatLintIssues NullLintFormatter _ = pure mempty
  124. formatLintIssues SimpleLintFormatter issues = concat <$> mapM fmt sortedIssues
  125. where
  126. sortedIssues = sortOn (((-1) *) . view priority) issues
  127. fmt :: LintIssue -> Reader AppEnv [Chunk T.Text]
  128. fmt i =
  129. sequence [ pure $ label i
  130. , pure $ chunk (" " <> i ^. summary <> "\n") & bold
  131. , pure $ chunk $ concat $ replicate 4 " "
  132. , pure $ chunk ( T.pack (i ^. location . filename)
  133. <> fmtLine (i ^. location . line)
  134. <> "\n"
  135. ) & underline & fore blue
  136. , fmtExplanation i
  137. ]
  138. fmtExplanation :: LintIssue -> Reader AppEnv (Chunk T.Text)
  139. fmtExplanation i = ask >>= \env -> return $ case env ^. opts . verbose of
  140. Normal -> mempty
  141. Verbose -> chunk
  142. ( maybe
  143. (i ^. explanation <> "\n")
  144. (\size -> indentWrap size 4 $ i ^. explanation)
  145. (env ^. terminalSize)
  146. ) & faint
  147. fmtLine :: Show a => Maybe a -> T.Text
  148. fmtLine = maybe mempty ((":" <>) . T.pack . show)
  149. label i = dye i ( "["
  150. <> T.take 1 (toText $ i ^. severity)
  151. <> "]" )
  152. dye i j = colorSeverity (i ^. severity) (chunk j)
  153. atTag :: ArrowXml a => String -> a XmlTree XmlTree
  154. atTag tag = deep (isElem >>> hasName tag)
  155. sread :: Read a => String -> a
  156. sread = read . T.pack
  157. sreadMay :: Read a => String -> Maybe a
  158. sreadMay = readMay . T.pack
  159. indentWrap :: Terminal.Window Int -> Int -> T.Text -> T.Text
  160. indentWrap size indentation text = foldMap wrap lines'
  161. where
  162. indent :: T.Text
  163. indent = concat $ replicate indentation " "
  164. lines' = filter (/= mempty) $ lines text
  165. wrap t
  166. | t == mempty = mempty
  167. | otherwise = let (as, bs) = T.splitAt (Terminal.width size - indentation) t
  168. in indent <> as <> "\n" <> wrap bs
  169. openXMLFile :: forall s b. FilePath -> IO (IOStateArrow s b XmlTree)
  170. openXMLFile = (readXMLFileHandle =<<) . getHandle
  171. where
  172. getHandle filepath
  173. | filepath == "-" = return stdin
  174. | otherwise = openFile filepath ReadMode
  175. readXMLFileHandle :: forall s b. Handle -> IO (IOStateArrow s b XmlTree)
  176. readXMLFileHandle h = do
  177. contents <- TIO.hGetContents h
  178. return $ readString [withWarnings yes] $ T.unpack contents
  179. readLintIssues :: IOSLA (XIOState ()) XmlTree XmlTree -> IO [LintIssue]
  180. readLintIssues doc =
  181. runX $ doc >>> selectIssues >>> parseIssues
  182. where
  183. parseIssues :: ArrowXml a => a XmlTree LintIssue
  184. parseIssues = proc i -> do
  185. severity' <- arr fromString <<< getAttrValue "severity" -< i
  186. summary' <- arr T.pack <<< getAttrValue "summary" -< i
  187. priority' <- arr sread <<< getAttrValue "priority" -< i
  188. explanation' <- arr T.pack <<< getAttrValue "explanation" -< i
  189. location' <- parseLocation -< i
  190. returnA -< LintIssue { _severity = severity'
  191. , _summary = summary'
  192. , _explanation = explanation'
  193. , _priority = priority'
  194. , _location = location'
  195. }
  196. parseLocation :: ArrowXml a => a XmlTree LintLocation
  197. parseLocation = atTag "location" >>> proc l -> do
  198. filename' <- getAttrValue "file" -< l
  199. line' <- arr sreadMay <<< getAttrValue "line" -< l
  200. column' <- arr sreadMay <<< getAttrValue "column" -< l
  201. returnA -< LintLocation { _filename = filename'
  202. , _line = line'
  203. , _column = column'
  204. }
  205. selectIssues :: ArrowXml a => a XmlTree XmlTree
  206. selectIssues = getChildren
  207. >>>
  208. isElem >>> hasName "issues"
  209. >>>
  210. hasAttrValue "format" (== supportedLintFormatVersion)
  211. >>>
  212. atTag "issue"