Skip to content

Commit ebbceed

Browse files
authored
Better formatting of result message in GitHub.
1 parent a1b3743 commit ebbceed

File tree

4 files changed

+130
-22
lines changed

4 files changed

+130
-22
lines changed

action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ outputs:
4242

4343
runs:
4444
using: docker
45-
image: Dockerfile # docker://ghcr.io/haskell-actions/hlint-scan:main
45+
image: docker://ghcr.io/haskell-actions/hlint-scan:main
4646
args:
4747
- binary=${{ inputs.binary }}
4848
- path=${{ inputs.path }}

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ the [Haskell Package Versioning Policy].
1111
## Unreleased
1212

1313
* Support `hints` input for explicitly specifying the HLint configuration file.
14+
* Better message formatting on GitHub.
1415

1516
## 0.4.1 - 2023-04-10
1617

src/Format.hs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,32 +47,44 @@ import Data.Text qualified as Text
4747
-- | Format text messages in result objects to be better readable on GitHub.
4848
formatMessages :: Value -> Value
4949
formatMessages (Object v) = Object $ mapWithKey formatRuns v
50-
formatMessages v = v
51-
52-
formatRuns :: Key -> Value -> Value
53-
formatRuns "runs" (Array vs) = Array $ fmap formatRun vs
54-
formatRuns _ v = v
50+
where
51+
formatRuns "runs" (Array us) = Array $ fmap formatRun us
52+
formatRuns _ u = u
5553

56-
formatRun :: Value -> Value
57-
formatRun (Object v) = Object $ mapWithKey formatResults v
58-
formatRun v = v
54+
formatRun (Object u) = Object $ mapWithKey formatResults u
55+
formatRun u = u
5956

60-
formatResults :: Key -> Value -> Value
61-
formatResults "results" (Array vs) = Array $ fmap formatResult vs
62-
formatResults _ v = v
57+
formatResults "results" (Array us) = Array $ fmap formatResult us
58+
formatResults _ u = u
6359

64-
formatResult :: Value -> Value
65-
formatResult (Object v) = Object $ mapWithKey formatMessage v
66-
formatResult v = v
60+
formatResult (Object u) = Object $ mapWithKey formatMessage u
61+
formatResult u = u
6762

68-
formatMessage :: Key -> Value -> Value
69-
formatMessage "message" (Object v) = Object $ mapWithKey formatText v
70-
formatMessage _ v = v
63+
formatMessage "message" (Object u) = Object $ mapWithKey formatText u
64+
formatMessage _ u = u
65+
formatMessages v = v
7166

67+
-- | Formats the text in a @message@ object better for GitHub.
68+
--
69+
-- Basically rewrites the text so that GitHub does not turn the message
70+
-- into mindlessly left-aligned lines of text. The specific rewriting
71+
-- was derived by trial and error; there appears to be no documentation
72+
-- as to what parts of Markdown syntax are effective in this context,
73+
-- unfortunately.
7274
formatText :: Key -> Value -> Value
73-
formatText "text" (String s) = String s'
75+
formatText "text" (String s) = String s''
7476
where
75-
l = Text.lines s
76-
s' | x:xs <- l = Text.unlines $ x : "<pre>" : xs ++ ["</pre>"]
77-
| [] <- l = s
77+
s' = Text.unlines $ format $ Text.lines s
78+
-- Replace all spaces with @&nbsp;@ so that GitHub does not collapse them.
79+
s'' = Text.replace " " "&nbsp;" s'
80+
-- Put an extra newline between separate pieces of content.
81+
-- I.e., between the general message, the code found,
82+
-- the suggested replacements, and any notes.
83+
-- The extra line is inserted as " " instead of ""
84+
-- because GitHub would otherwise collapse them.
85+
format (x : xs@(x' : _))
86+
| not (Text.isPrefixOf " " x') = x : " " : format xs
87+
| otherwise = x : format xs
88+
format (x : xs) = x : format xs
89+
format [] = []
7890
formatText _ v = v

test/FormatSpec.hs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
{-
2+
Copyright 2023 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-}
16+
17+
-- |
18+
-- Description: Tests for the "Format" module.
19+
-- Copyright: Copyright 2023 Google LLC
20+
-- License: Apache-2.0
21+
-- Maintainer: [email protected]
22+
module FormatSpec (spec) where
23+
24+
import Data.Aeson
25+
import Data.Aeson.KeyMap qualified as KeyMap
26+
import Data.Char
27+
import Data.Text (Text)
28+
import Data.Text qualified as Text
29+
import Data.Vector qualified as Vector
30+
import Format
31+
import Test.Hspec
32+
import Test.Hspec.QuickCheck
33+
import Test.QuickCheck
34+
import Test.QuickCheck.Instances.Text ()
35+
36+
spec :: Spec
37+
spec = do
38+
describe "formatMessages" $ do
39+
it "formats a particular message" $
40+
formatMessages
41+
( objectWithMessage $
42+
Text.unlines
43+
[ "Bad.hs:16:9-20: Warning: Use /=",
44+
"Found:",
45+
" not (a == b)",
46+
"Perhaps:",
47+
" a /= b",
48+
"Note: incorrect if either value is NaN"
49+
]
50+
)
51+
`shouldBe` objectWithMessage
52+
( Text.unlines
53+
[ "Bad.hs:16:9-20:&nbsp;Warning:&nbsp;Use&nbsp;/=",
54+
"&nbsp;&nbsp;",
55+
"Found:",
56+
"&nbsp;&nbsp;not&nbsp;(a&nbsp;==&nbsp;b)",
57+
"&nbsp;&nbsp;",
58+
"Perhaps:",
59+
"&nbsp;&nbsp;a&nbsp;/=&nbsp;b",
60+
"&nbsp;&nbsp;",
61+
"Note:&nbsp;incorrect&nbsp;if&nbsp;either&nbsp;value&nbsp;is&nbsp;NaN"
62+
]
63+
)
64+
65+
prop "formats messages in general" $
66+
forAll (listOf chooseSection) $ \sections ->
67+
let message = mconcat sections
68+
message' = Text.replace " " "&nbsp;" $ Text.intercalate " \n" sections
69+
in counterexample (show message) $
70+
counterexample (show message') $
71+
formatMessages (objectWithMessage message) `shouldBe` objectWithMessage message'
72+
73+
objectWithMessage :: Text -> Value
74+
objectWithMessage message =
75+
Object . KeyMap.singleton "runs" $
76+
Array . Vector.singleton . Object . KeyMap.singleton "results" $
77+
Array . Vector.singleton . Object . KeyMap.singleton "message" $
78+
Object . KeyMap.singleton "text" $
79+
String message
80+
81+
chooseSection :: Gen Text
82+
chooseSection = do
83+
note <- arbitrary `suchThat` isNonEmptyLine
84+
codelines <- map (" " <>) <$> arbitrary `suchThat` all isNonEmptyLine
85+
return $ Text.unlines $ note : codelines
86+
where
87+
isNonEmptyLine s =
88+
not
89+
( Text.null s
90+
|| Text.take 1 s == " "
91+
|| Text.elem '\n' s
92+
|| Text.elem '\r' s
93+
|| Text.all isSpace s
94+
)
95+
&& Text.all (\c -> isAlphaNum c || c == ' ') s

0 commit comments

Comments
 (0)