summaryrefslogtreecommitdiff
path: root/linters/lint-notes
diff options
context:
space:
mode:
Diffstat (limited to 'linters/lint-notes')
-rw-r--r--linters/lint-notes/Main.hs62
-rw-r--r--linters/lint-notes/Makefile17
-rw-r--r--linters/lint-notes/Notes.hs186
-rw-r--r--linters/lint-notes/check.sh30
-rw-r--r--linters/lint-notes/ghc.mk18
-rw-r--r--linters/lint-notes/lint-notes.cabal21
-rw-r--r--linters/lint-notes/test25
7 files changed, 359 insertions, 0 deletions
diff --git a/linters/lint-notes/Main.hs b/linters/lint-notes/Main.hs
new file mode 100644
index 0000000000..02ee3f11d4
--- /dev/null
+++ b/linters/lint-notes/Main.hs
@@ -0,0 +1,62 @@
+import qualified Data.Set as S
+import System.Process
+import System.Environment
+
+import Notes
+
+usage :: IO a
+usage = do
+ putStrLn $ unlines
+ [ "usage:"
+ , " lint-notes <mode>"
+ , " lint-notes <mode> @<response-file>"
+ , " lint-notes <mode> <file>"
+ , ""
+ , "valid modes:"
+ , " dump dump all Note definitions and references"
+ , " defs dump all Note definitions"
+ , " refs dump all Note references"
+ , " unreferenced dump all unreferenced Note definitions"
+ , " broken-refs dump all references to missing Notes"
+ ]
+ fail "invalid usage"
+
+main :: IO ()
+main = do
+ args <- getArgs
+
+ let printNoteDefs = putStrLn . unlines . map showNoteDef
+ printNoteRefs = putStrLn . unlines . map showNoteRef
+ printNoteRefsSugg (bad, sugg) = do
+ putStrLn . showNoteRef $ bad
+ putStrLn $ " >" ++ showNoteDef sugg
+
+
+ parseMode :: String -> Maybe (NoteDb -> IO ())
+ parseMode "dump" = Just $ putStrLn . showNoteDb
+ parseMode "unreferenced" = Just $ printNoteDefs . S.toList . unreferencedNotes
+ parseMode "defs" = Just $ printNoteDefs . allNoteDefs
+ parseMode "refs" = Just $ printNoteRefs . allNoteRefs
+ parseMode "broken-refs" = Just $ printNoteRefs . map fst . brokenNoteRefs
+ parseMode "broken-refs-suggest" = Just $ mapM_ printNoteRefsSugg . brokenNoteRefs
+ parseMode _ = Nothing
+
+ (mode, files) <- case args of
+ [mode, "@-"] -> do
+ files <- lines <$> getContents
+ return (parseMode mode, files)
+ [mode, '@':respFile] -> do
+ files <- lines <$> readFile respFile
+ return (parseMode mode, files)
+ [mode] -> do
+ let excludeList =
+ [ "testsuite/tests/linters/notes.stdout"
+ , "linters/lint-notes/test" ]
+ files <- lines <$> readProcess "git" ["ls-tree", "--name-only", "-r", "HEAD"] ""
+ return (parseMode mode, filter (`notElem` excludeList) files)
+ _ -> usage
+
+ case mode of
+ Just run -> filesNotes files >>= run
+ Nothing -> return ()
+
diff --git a/linters/lint-notes/Makefile b/linters/lint-notes/Makefile
new file mode 100644
index 0000000000..71500f0147
--- /dev/null
+++ b/linters/lint-notes/Makefile
@@ -0,0 +1,17 @@
+# -----------------------------------------------------------------------------
+#
+# (c) 2009 The University of Glasgow
+#
+# This file is part of the GHC build system.
+#
+# To understand how the build system works and how to modify it, see
+# https://gitlab.haskell.org/ghc/ghc/wikis/building/architecture
+# https://gitlab.haskell.org/ghc/ghc/wikis/building/modifying
+#
+# -----------------------------------------------------------------------------
+
+dir = linters/lint-notes
+TOP = ../..
+include $(TOP)/mk/sub-makefile.mk
+
+FAST_MAKE_OPTS += stage=none
diff --git a/linters/lint-notes/Notes.hs b/linters/lint-notes/Notes.hs
new file mode 100644
index 0000000000..cf267d8d67
--- /dev/null
+++ b/linters/lint-notes/Notes.hs
@@ -0,0 +1,186 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns #-}
+
+module Notes where
+
+import Data.Either
+import Data.Foldable
+import qualified Data.ByteString as BS
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import qualified Data.Map.Strict as M
+import qualified Data.Set as S
+import System.Directory (doesFileExist)
+import Data.Array
+import Data.List (sortBy)
+import Data.Ord
+
+data SrcLoc = SrcLoc { fileName :: FilePath
+ , row :: !Int
+ , column :: !Int
+ }
+ deriving (Eq, Ord, Show)
+
+showSrcLoc :: SrcLoc -> String
+showSrcLoc loc =
+ concat [fileName loc, ":", show (row loc), ":", show (column loc), ":"]
+
+newtype NoteName = NoteName T.Text
+ deriving (Eq, Ord, Show)
+
+showNoteName :: NoteName -> String
+showNoteName (NoteName x) = "Note [" <> T.unpack x <> "]"
+
+data NoteDef = NoteDef { noteDefSrcLoc :: !SrcLoc
+ , noteDefName :: !NoteName
+ }
+ deriving (Eq, Ord, Show)
+
+showNoteDef :: NoteDef -> String
+showNoteDef (NoteDef{noteDefSrcLoc=loc, noteDefName=name}) =
+ "def " <> showSrcLoc loc <> " " <> showNoteName name
+
+data NoteRef = NoteRef { noteRefSrcLoc :: !SrcLoc
+ , noteRefName :: !NoteName
+ }
+ deriving (Eq, Ord, Show)
+
+showNoteRef :: NoteRef -> String
+showNoteRef (NoteRef{noteRefSrcLoc=loc, noteRefName=name}) =
+ "ref " <> showSrcLoc loc <> " " <> showNoteName name
+
+findNotes :: FilePath -> T.Text -> [Either NoteRef NoteDef]
+findNotes fname t =
+ go 1 (T.lines t)
+ where
+ go :: Int -> [T.Text] -> [Either NoteRef NoteDef]
+ -- Note definitions:
+ -- We look for a "Note [" token with a "~~~" rule beneath it.
+ go !lineNo (l0 : l1 : ls)
+ | hasRule = Right (NoteDef srcLoc name) : go (lineNo+2) ls
+ where
+ (prefix, rest) = T.breakOn "Note [" l0
+ startCol = T.length prefix
+ hasRule = T.take 3 (T.drop startCol l1) == "~~~"
+ srcLoc = SrcLoc fname lineNo startCol
+ name = NoteName $ T.takeWhile (/= ']') $ T.drop 6 rest
+
+ -- Note references:
+ -- We look for a "Note [...]", strip away any beginning-of-line
+ -- comment symbols, and merge whitespace.
+ go lineNo (l0 : ls) =
+ [ Left (NoteRef srcLoc (NoteName name))
+ | (prefix, rest) <- T.breakOnAll "Note [" l0
+ , let startCol = T.length prefix
+ srcLoc = SrcLoc fname lineNo startCol
+ (name, suffix) = T.breakOn "]" (T.drop 6 rest<>" "<>T.concat (map stripBeginningOfLineComment $ take 1 ls))
+ , "]" `T.isPrefixOf` suffix
+ ] ++ go (lineNo+1) ls
+
+ go _lineNo [] = []
+
+stripBeginningOfLineComment :: T.Text -> T.Text
+stripBeginningOfLineComment = T.pack . go . T.unpack
+ where
+ -- This implements the following regular expression substitution:
+ --
+ -- s/$ *[(\-\- )\#( \* )] */ /
+
+ go :: String -> String
+ go ('#':rest) = finish rest
+ go ('-':'-':rest) = finish rest
+ go (' ':'*':rest) = finish rest
+ go ('/':'/':rest) = finish rest
+ go (' ':rest) = go rest
+ go xs = finish xs
+
+ finish :: String -> String
+ finish = dropWhile (==' ')
+
+data NoteDb = NoteDb { noteRefs :: M.Map FilePath (S.Set NoteRef)
+ , noteDefs :: M.Map NoteName (S.Set NoteDef)
+ }
+
+instance Monoid NoteDb where
+ mempty = NoteDb M.empty M.empty
+
+instance Semigroup NoteDb where
+ NoteDb a b <> NoteDb c d =
+ NoteDb (M.unionWith (<>) a c) (M.unionWith (<>) b d)
+
+allNoteDefs :: NoteDb -> [NoteDef]
+allNoteDefs db =
+ [ def
+ | defs <- M.elems (noteDefs db)
+ , def <- S.toList defs
+ ]
+
+allNoteRefs :: NoteDb -> [NoteRef]
+allNoteRefs db =
+ [ ref
+ | (_fname, refs) <- M.toList (noteRefs db)
+ , ref <- S.toList refs
+ ]
+
+showNoteDb :: NoteDb -> String
+showNoteDb db = unlines $
+ map showNoteRef (allNoteRefs db)
+ ++
+ map showNoteDef (allNoteDefs db)
+
+filesNotes :: [FilePath]
+ -> IO NoteDb
+filesNotes = fmap mconcat . mapM fileNotes
+
+fileNotes :: FilePath -> IO NoteDb
+fileNotes fname = do
+ is_file <- doesFileExist fname
+ if is_file
+ then do
+ bs <- BS.readFile fname
+ return $ case T.decodeUtf8' bs of
+ Left _ -> mempty
+ Right t ->
+ let (refs, defs) = partitionEithers (findNotes fname t)
+ in NoteDb
+ { noteRefs = M.singleton fname (S.fromList refs)
+ , noteDefs = M.fromList
+ [ (noteDefName def, S.singleton def)
+ | def <- defs
+ ]
+ }
+ else return mempty
+
+brokenNoteRefs :: NoteDb -> [(NoteRef, NoteDef)]
+brokenNoteRefs db =
+ [ (ref, best_match)
+ | (_fname, refs) <- M.toList (noteRefs db)
+ , ref <- S.toList refs
+ , Nothing <- pure $ M.lookup (noteRefName ref) (noteDefs db)
+ , let best_match = bestLev (show (noteRefName ref)) (concatMap S.toList (M.elems (noteDefs db)))
+ ]
+
+bestLev :: String -> [NoteDef] -> NoteDef
+bestLev x ds = head $ sortBy (comparing (\d -> lev x (show (noteDefName d)))) ds
+
+
+lev:: (Eq a) => [a] -> [a] -> Int
+lev xs ys = levMemo ! (n, m)
+ where levMemo = array ((0,0),(n,m)) [((i,j),lev' i j) | i <- [0..n], j <- [0..m]]
+ n = length xs
+ m = length ys
+ xa = listArray (1, n) xs
+ ya = listArray (1, m) ys
+ lev' 0 v = v
+ lev' u 0 = u
+ lev' u v
+ | xa ! u == ya ! v = levMemo ! (u-1, v-1)
+ | otherwise = 1 + minimum [levMemo ! (u, v-1),
+ levMemo ! (u-1, v),
+ levMemo ! (u-1, v-1)]
+
+unreferencedNotes :: NoteDb -> S.Set NoteDef
+unreferencedNotes db =
+ fold $ noteDefs db `M.withoutKeys` referencedNotes
+ where
+ referencedNotes = S.fromList $ map noteRefName (allNoteRefs db)
diff --git a/linters/lint-notes/check.sh b/linters/lint-notes/check.sh
new file mode 100644
index 0000000000..cd87900f9b
--- /dev/null
+++ b/linters/lint-notes/check.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+set -e
+
+CABAL_INSTALL="${CABAL_INSTALL:-cabal}"
+GHC="${GHC:-ghc}"
+
+cd "$(dirname $0)"
+"$CABAL_INSTALL" build -w "$GHC"
+bin="$("$CABAL_INSTALL" list-bin -w "$GHC" lint-notes)"
+cd "$(git rev-parse --show-toplevel)"
+"$bin" broken-refs \
+ | grep -v "linters/lint-notes/expected-broken-note-refs:" \
+ | sed 's/:[0-9]\+:[0-9]\+:/:/' \
+ > broken-note-refs
+
+if diff -q linters/lint-notes/expected-broken-note-refs broken-note-refs; then
+ printf "No unexpected broken note references"
+else
+ printf "Found unexpected broken note references:\n\n"
+ diff -u linters/lint-notes/expected-broken-note-refs broken-note-refs || true
+ if [[ "$1" == "-a" ]]; then
+ cp broken-note-refs linters/lint-notes/expected-broken-note-refs
+ printf "\n"
+ printf "Accepted new broken note references."
+ else
+ exit 1
+ fi
+fi
+
diff --git a/linters/lint-notes/ghc.mk b/linters/lint-notes/ghc.mk
new file mode 100644
index 0000000000..bd4e97a85f
--- /dev/null
+++ b/linters/lint-notes/ghc.mk
@@ -0,0 +1,18 @@
+# -----------------------------------------------------------------------------
+#
+# (c) 2009 The University of Glasgow
+#
+# This file is part of the GHC build system.
+#
+# To understand how the build system works and how to modify it, see
+# https://gitlab.haskell.org/ghc/ghc/wikis/building/architecture
+# https://gitlab.haskell.org/ghc/ghc/wikis/building/modifying
+#
+# -----------------------------------------------------------------------------
+
+linters/lint-notes_USES_CABAL = YES
+linters/lint-notes_PACKAGE = lint-notes
+linters/lint-notes_dist-install_PROGNAME = lint-notes
+linters/lint-notes_dist-install_INSTALL = NO
+linters/lint-notes_dist-install_INSTALL_INPLACE = YES
+$(eval $(call build-prog,linters/lint-notes,dist-install,1))
diff --git a/linters/lint-notes/lint-notes.cabal b/linters/lint-notes/lint-notes.cabal
new file mode 100644
index 0000000000..9896e74766
--- /dev/null
+++ b/linters/lint-notes/lint-notes.cabal
@@ -0,0 +1,21 @@
+cabal-version: 2.4
+name: lint-notes
+version: 0.1.0.0
+synopsis: A tool for querying and checking GHC Notes
+bug-reports: https://gitlab.haskell.org/ghc/ghc
+license: BSD-3-Clause
+author: Ben Gamari
+maintainer: ben@smart-cactus.org
+copyright: (c) 2022 Ben Gamari
+
+executable lint-notes
+ main-is: Main.hs
+ other-modules: Notes
+ build-depends: base >= 4 && < 5 ,
+ bytestring,
+ containers,
+ directory,
+ text,
+ process,
+ array
+ default-language: Haskell2010
diff --git a/linters/lint-notes/test b/linters/lint-notes/test
new file mode 100644
index 0000000000..3eb1e0466e
--- /dev/null
+++ b/linters/lint-notes/test
@@ -0,0 +1,25 @@
+Note [Turtles]
+~~~~~~~~~~~~~~~
+This is note 1. See Note [Wombats are toes].
+
+/* Note [Wombats are toes]
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ This is another Note.
+ */
+
+See Note [This is a
+Note reference broken across lines].
+
+ // This is another Note [Broken
+ // across a line in a C++ comment].
+
+ /*
+ * This is another Note [Broken
+ * across a line in a C comment].
+ */
+
+ -- This is another Note [Broken
+ -- across a line in a Haskell comment].
+
+ # This is another Note [Broken
+ # across a line in a Python comment].