Skip to content

Commit 7189218

Browse files
committed
[#226] Change output formatting
Problem: multiple issues with the CLI output 1. In the --help output, metavars GLOB_PATTERN and REPOSITORY_TYPE used spaces instead of underscores. Variables typically don't contain spaces in their names, so it looked confusing. 2. Incorrect or unintuitive coloring of the output. For example, filenames were dim even though they're important and should stand out, the report of success wasn't green, error messages weren't red, etc. 3. Excessive indentation and empty lines that made it difficult to visually parse the error report. Also the use of the exotic '➥' character that looks bad in many font configurations. 4. Filenames and line numbers were far apart, making it impossible to Ctrl+Click to jump to the source of the error. 5. In case of connection failure, the output was too verbose and platform-dependent. Solution: 1. Adjust the pretty-printing functions and update test output. 2. Extend VerifyError with a constructor for connection failures.
1 parent 671f527 commit 7189218

File tree

39 files changed

+687
-992
lines changed

39 files changed

+687
-992
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ Unreleased
5656
+ Now Xrefcheck is able to follow relative redirects.
5757
* [#262](https://github.com/serokell/xrefcheck/pull/262)
5858
+ Now Xrefcheck includes a scanner that verifies the repository symlinks.
59+
* [#307](https://github.com/serokell/xrefcheck/pull/307)
60+
+ The output of Xrefcheck is now more legible.
5961

6062
0.2.2
6163
==========

docs/output-sample/output-sample.png

159 KB
Loading

package.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ library:
9393
- filepath
9494
- fmt
9595
- ftp-client
96+
- crypton-connection
9697
- Glob
9798
- http-client
9899
- http-types

src/Xrefcheck/CLI.hs

+2-2
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ exclusionOptionsParser :: Parser ExclusionOptions
208208
exclusionOptionsParser = do
209209
eoIgnore <- many . globOption $
210210
long "ignore" <>
211-
metavar "GLOB PATTERN" <>
211+
metavar "GLOB_PATTERN" <>
212212
help "Ignore these files. References to them will fail verification,\
213213
\ and references from them will not be verified.\
214214
\ Glob patterns that contain wildcards MUST be enclosed\
@@ -237,7 +237,7 @@ dumpConfigOptions = hsubparser $
237237
option repoTypeReadM $
238238
short 't' <>
239239
long "type" <>
240-
metavar "REPOSITORY TYPE" <>
240+
metavar "REPOSITORY_TYPE" <>
241241
help [int||
242242
Git repository type. \
243243
Can be (#{intercalate " | " $ map show allFlavors}). \

src/Xrefcheck/Command.hs

+2-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ defaultAction Options{..} = do
9292
verifyRepo rw fullConfig oMode repoInfo
9393

9494
case verifyErrors verifyRes of
95-
Nothing | null scanErrs -> fmtLn "All repository links are valid."
95+
Nothing | null scanErrs ->
96+
fmtLn $ colorIfNeeded Green "All repository links are valid."
9697
Nothing -> exitFailure
9798
Just verifyErrs -> do
9899
unless (null scanErrs) $ fmt "\n"

src/Xrefcheck/Core.hs

+3-5
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,8 @@ instance FromJSON Flavor where
6363
newtype Position = Position (Maybe Text)
6464
deriving stock (Show, Eq, Generic)
6565

66-
instance Given ColorMode => Buildable Position where
67-
build (Position pos) = case pos of
68-
Nothing -> ""
69-
Just p -> styleIfNeeded Faint $ "at src:" <> build p
66+
instance Buildable Position where
67+
build (Position pos) = maybe "" build pos
7068

7169
-- | Full info about a reference.
7270
data Reference = Reference
@@ -274,7 +272,7 @@ instance Given ColorMode => Buildable Reference where
274272
instance Given ColorMode => Buildable AnchorType where
275273
build = styleIfNeeded Faint . \case
276274
HeaderAnchor l -> colorIfNeeded Green ("header " <> headerLevelToRoman l)
277-
HandAnchor -> colorIfNeeded Yellow "hand made"
275+
HandAnchor -> colorIfNeeded Yellow "handmade"
278276
BiblioAnchor -> colorIfNeeded Cyan "biblio"
279277
where
280278
headerLevelToRoman = \case

src/Xrefcheck/Scan.hs

+15-18
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import Control.Lens (_1, makeLensesWith, (%~))
3737
import Data.Aeson (FromJSON (..), genericParseJSON, withText)
3838
import Data.Map qualified as M
3939
import Data.Reflection (Given)
40-
import Fmt (Buildable (..), fmt)
40+
import Fmt (Buildable (..), Builder, fmtLn)
4141
import System.Directory (doesDirectoryExist, pathIsSymbolicLink)
4242
import System.Process (cwd, readCreateProcess, shell)
4343
import Text.Interpolation.Nyan
@@ -120,23 +120,20 @@ mkGatherScanError seFile ScanError{sePosition, seDescription} = ScanError
120120
, seDescription
121121
}
122122

123-
instance Given ColorMode => Buildable (ScanError 'Gather) where
124-
build ScanError{..} = [int||
125-
In file #{styleIfNeeded Faint (styleIfNeeded Bold seFile)}
126-
scan error #{sePosition}:
127-
128-
#{seDescription}
129-
130-
|]
123+
pprScanErr :: Given ColorMode => ScanError 'Gather -> Builder
124+
pprScanErr ScanError{..} = hdr <> "\n" <> interpolateIndentF 2 msg <> "\n"
125+
where
126+
hdr, msg :: Builder
127+
hdr =
128+
styleIfNeeded Bold (build seFile <> ":" <> build sePosition <> ": ") <>
129+
colorIfNeeded Red "scan error:"
130+
msg = build seDescription
131131

132132
reportScanErrs :: Given ColorMode => NonEmpty (ScanError 'Gather) -> IO ()
133-
reportScanErrs errs = fmt
134-
[int||
135-
=== Scan errors found ===
136-
137-
#{interpolateIndentF 2 (interpolateBlockListF' "➥ " build errs)}
138-
Scan errors dumped, #{length errs} in total.
139-
|]
133+
reportScanErrs errs = do
134+
traverse_ (fmtLn . pprScanErr) errs
135+
fmtLn $ colorIfNeeded Red $
136+
"Scan errors dumped, " <> build (length errs) <> " in total."
140137

141138
data ScanErrorDescription
142139
= LinkErr
@@ -152,8 +149,8 @@ instance Buildable ScanErrorDescription where
152149
markdown or right after comments at the top|]
153150
ParagraphErr txt -> [int||Expected a PARAGRAPH after \
154151
"ignore paragraph" annotation, but found #{txt}|]
155-
UnrecognisedErr txt -> [int||Unrecognised option "#{txt}" perhaps you meant \
156-
<"ignore link"|"ignore paragraph"|"ignore all">|]
152+
UnrecognisedErr txt -> [int||Unrecognised option "#{txt}"
153+
Perhaps you meant <"ignore link"|"ignore paragraph"|"ignore all">|]
157154

158155
firstFileSupport :: [FileSupport] -> FileSupport
159156
firstFileSupport fs isSymlink =

src/Xrefcheck/Verify.hs

+63-20
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ import Data.Text.Metrics (damerauLevenshteinNorm)
4646
import Data.Time (UTCTime, defaultTimeLocale, formatTime, readPTime, rfc822DateFormat)
4747
import Data.Time.Clock.POSIX (getPOSIXTime)
4848
import Data.Traversable (for)
49-
import Fmt (Buildable (..), Builder, fmt, maybeF, nameF)
49+
import Fmt (Buildable (..), Builder, fmt, fmtLn, maybeF, nameF)
5050
import GHC.Exts qualified as Exts
5151
import GHC.Read (Read (readPrec))
52+
import Network.Connection qualified as N.C
5253
import Network.FTP.Client
5354
(FTPException (..), FTPResponse (..), ResponseStatus (..), login, nlst, size, withFTP, withFTPS)
5455
import Network.HTTP.Client
@@ -107,13 +108,6 @@ data WithReferenceLoc a = WithReferenceLoc
107108
, wrlItem :: a
108109
}
109110

110-
instance (Given ColorMode, Buildable a) => Buildable (WithReferenceLoc a) where
111-
build WithReferenceLoc{..} = [int||
112-
In file #{styleIfNeeded Faint (styleIfNeeded Bold wrlFile)}
113-
bad #{wrlReference}
114-
#{wrlItem}
115-
|]
116-
117111
-- | Contains a name of a domain, examples:
118112
-- @DomainName "github.com"@,
119113
-- @DomainName "localhost"@,
@@ -137,6 +131,7 @@ data VerifyError
137131
| ExternalFtpException FTPException
138132
| FtpEntryDoesNotExist FilePath
139133
| ExternalResourceSomeError Text
134+
| ExternalResourceConnectionFailure
140135
| RedirectChainCycle RedirectChain
141136
| RedirectMissingLocation RedirectChain
142137
| RedirectChainLimit RedirectChain
@@ -147,8 +142,8 @@ data ResponseResult
147142
= RRDone
148143
| RRFollow Text
149144

150-
instance Given ColorMode => Buildable VerifyError where
151-
build = \case
145+
pprVerifyErr' :: Given ColorMode => VerifyError -> Builder
146+
pprVerifyErr' = \case
152147
LocalFileDoesNotExist file ->
153148
[int||
154149
File does not exist:
@@ -256,6 +251,11 @@ instance Given ColorMode => Buildable VerifyError where
256251
#{err}
257252
|]
258253

254+
ExternalResourceConnectionFailure ->
255+
[int||
256+
Connection failure
257+
|]
258+
259259
RedirectChainCycle chain ->
260260
[int||
261261
Cycle found in the following redirect chain:
@@ -304,15 +304,48 @@ incTotalCounter rc = rc {rcTotalRetries = rcTotalRetries rc + 1}
304304
incTimeoutCounter :: RetryCounter -> RetryCounter
305305
incTimeoutCounter rc = rc {rcTimeoutRetries = rcTimeoutRetries rc + 1}
306306

307+
pprVerifyErr :: Given ColorMode => WithReferenceLoc VerifyError -> Builder
308+
pprVerifyErr wrl = hdr <> "\n" <> interpolateIndentF 2 msg
309+
where
310+
WithReferenceLoc{wrlFile, wrlReference, wrlItem} = wrl
311+
Reference{rName, rInfo} = wrlReference
312+
313+
hdr, msg :: Builder
314+
hdr =
315+
styleIfNeeded Bold (build wrlFile <> ":" <> build (rPos wrlReference) <> ": ") <>
316+
colorIfNeeded Red "bad reference:"
317+
msg =
318+
"The reference to " <> show rName <> " failed verification.\n" <>
319+
mconcat (map (\info -> "- " <> info <> "\n") ref_infos) <>
320+
pprVerifyErr' wrlItem
321+
322+
ref_infos :: [Builder]
323+
ref_infos = case rInfo of
324+
RIFile ReferenceInfoFile{..} ->
325+
case rifLink of
326+
FLLocal ->
327+
case rifAnchor of
328+
Nothing -> []
329+
Just anc -> ["anchor " <> paren (styleIfNeeded Faint "file-local") <> ": " <> build anc]
330+
FLRelative link ->
331+
["link " <> paren (styleIfNeeded Faint "relative") <> ": " <> build link] ++
332+
(case rifAnchor of
333+
Nothing -> []
334+
Just anc -> ["anchor: " <> build anc])
335+
FLAbsolute link ->
336+
["link " <> paren (styleIfNeeded Faint "absolute") <> ": " <> build link] ++
337+
(case rifAnchor of
338+
Nothing -> []
339+
Just anc -> ["anchor: " <> build anc])
340+
RIExternal (ELUrl url) -> ["link " <> paren (styleIfNeeded Faint "external") <> ": " <> build url]
341+
RIExternal (ELOther url) -> ["link: " <> build url]
342+
307343
reportVerifyErrs
308344
:: Given ColorMode => NonEmpty (WithReferenceLoc VerifyError) -> IO ()
309-
reportVerifyErrs errs = fmt
310-
[int||
311-
=== Invalid references found ===
312-
313-
#{interpolateIndentF 2 (interpolateBlockListF' "➥ " build errs)}
314-
Invalid references dumped, #{length errs} in total.
315-
|]
345+
reportVerifyErrs errs = do
346+
traverse_ (fmtLn . pprVerifyErr) errs
347+
fmtLn $ colorIfNeeded Red $
348+
"Invalid references dumped, " <> build (length errs) <> " in total."
316349

317350
data RetryAfter = Date UTCTime | Seconds (Time Second)
318351
deriving stock (Show, Eq)
@@ -708,7 +741,7 @@ checkExternalResource followed config@Config{..} link
708741
let maxTime = Time @Second $ unTime ncExternalRefCheckTimeout * timeoutFrac
709742

710743
reqRes <- catch (liftIO (timeout maxTime $ reqLink $> RRDone)) $
711-
(Just <$>) <$> interpretErrors uri
744+
(Just <$>) <$> interpretHttpErrors uri
712745

713746
case reqRes of
714747
Nothing -> throwError $ ExternalHttpTimeout $ extractHost uri
@@ -730,9 +763,13 @@ checkExternalResource followed config@Config{..} link
730763
, (405 ==) -- method mismatch
731764
]
732765

733-
interpretErrors uri = \case
766+
interpretHttpErrors :: URI -> Network.HTTP.Req.HttpException -> ExceptT VerifyError IO ResponseResult
767+
interpretHttpErrors uri = \case
734768
JsonHttpException _ -> error "External link JSON parse exception"
735-
VanillaHttpException err -> case err of
769+
VanillaHttpException err -> interpretHttpErrors' uri err
770+
771+
interpretHttpErrors' :: URI -> Network.HTTP.Client.HttpException -> ExceptT VerifyError IO ResponseResult
772+
interpretHttpErrors' uri = \case
736773
InvalidUrlException{} -> error "External link URL invalid exception"
737774
HttpExceptionRequest _ exc -> case exc of
738775
StatusCodeException resp _
@@ -765,6 +802,12 @@ checkExternalResource followed config@Config{..} link
765802
redirectLocation = fmap decodeUtf8
766803
. lookup "Location"
767804
$ responseHeaders resp
805+
806+
ConnectionFailure _ -> throwError ExternalResourceConnectionFailure
807+
InternalException e
808+
| Just (N.C.HostCannotConnect _ _) <- fromException e
809+
-> throwError ExternalResourceConnectionFailure
810+
768811
other -> throwError $ ExternalResourceSomeError $ show other
769812
where
770813
retryAfterInfo :: Response a -> Maybe RetryAfter
+23-29
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,26 @@
1-
=== Invalid references found ===
1+
a.md:16:1-43: bad reference:
2+
The reference to "ambiguous anchor in this file" failed verification.
3+
- anchor (file-local): some-text
4+
Ambiguous reference to anchor 'some-text'
5+
In file a.md
6+
It could refer to either:
7+
- some-text (header I) 6:1-11
8+
- some-text (header I) 8:1-15
9+
- some-text (header II) 12:1-12
10+
Use of ambiguous anchors is discouraged because the target
11+
can change silently while the document containing it evolves.
212

3-
➥ In file a.md
4-
bad reference (file-local) at src:16:1-43:
5-
- text: "ambiguous anchor in this file"
6-
- anchor: some-text
7-
8-
Ambiguous reference to anchor 'some-text'
9-
In file a.md
10-
It could refer to either:
11-
- some-text (header I) at src:6:1-11
12-
- some-text (header I) at src:8:1-15
13-
- some-text (header II) at src:12:1-12
14-
Use of ambiguous anchors is discouraged because the target
15-
can change silently while the document containing it evolves.
16-
17-
➥ In file b.md
18-
bad reference (relative) at src:7:1-48:
19-
- text: "ambiguous anchor in other file"
20-
- link: a.md
21-
- anchor: some-text
22-
23-
Ambiguous reference to anchor 'some-text'
24-
In file a.md
25-
It could refer to either:
26-
- some-text (header I) at src:6:1-11
27-
- some-text (header I) at src:8:1-15
28-
- some-text (header II) at src:12:1-12
29-
Use of ambiguous anchors is discouraged because the target
30-
can change silently while the document containing it evolves.
13+
b.md:7:1-48: bad reference:
14+
The reference to "ambiguous anchor in other file" failed verification.
15+
- link (relative): a.md
16+
- anchor: some-text
17+
Ambiguous reference to anchor 'some-text'
18+
In file a.md
19+
It could refer to either:
20+
- some-text (header I) 6:1-11
21+
- some-text (header I) 8:1-15
22+
- some-text (header II) 12:1-12
23+
Use of ambiguous anchors is discouraged because the target
24+
can change silently while the document containing it evolves.
3125

3226
Invalid references dumped, 2 in total.
+15-23
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,19 @@
1-
=== Invalid references found ===
1+
a.md:12:1-13: bad reference:
2+
The reference to "broken" failed verification.
3+
- anchor (file-local): h3
4+
Anchor 'h3' is not present, did you mean:
5+
- h1 (header I) 6:1-4
6+
- h2 (header II) 8:1-5
27

3-
➥ In file a.md
4-
bad reference (file-local) at src:12:1-13:
5-
- text: "broken"
6-
- anchor: h3
8+
a.md:14:1-18: bad reference:
9+
The reference to "broken" failed verification.
10+
- anchor (file-local): heading
11+
Anchor 'heading' is not present, did you mean:
12+
- the-heading (header I) 10:1-13
713

8-
Anchor 'h3' is not present, did you mean:
9-
- h1 (header I) at src:6:1-4
10-
- h2 (header II) at src:8:1-5
11-
12-
➥ In file a.md
13-
bad reference (file-local) at src:14:1-18:
14-
- text: "broken"
15-
- anchor: heading
16-
17-
Anchor 'heading' is not present, did you mean:
18-
- the-heading (header I) at src:10:1-13
19-
20-
➥ In file a.md
21-
bad reference (file-local) at src:16:1-31:
22-
- text: "broken"
23-
- anchor: really-unique-anchor
24-
25-
Anchor 'really-unique-anchor' is not present
14+
a.md:16:1-31: bad reference:
15+
The reference to "broken" failed verification.
16+
- anchor (file-local): really-unique-anchor
17+
Anchor 'really-unique-anchor' is not present
2618

2719
Invalid references dumped, 3 in total.

0 commit comments

Comments
 (0)