diff --git a/LICENSE.markdown b/LICENSE.markdown
deleted file mode 100644
--- a/LICENSE.markdown
+++ /dev/null
@@ -1,13 +0,0 @@
-Copyright 2022 Taylor Fausak
-
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,13 @@
+Copyright 2024 Taylor Fausak
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -1,10 +1,7 @@
 # Splint
 
-[![CI](https://github.com/tfausak/splint/workflows/CI/badge.svg)](https://github.com/tfausak/splint/actions/new)
-[![Hackage](https://img.shields.io/hackage/v/splint)](https://hackage.haskell.org/package/splint)
-[![Stackage](https://www.stackage.org/package/splint/badge/nightly?label=stackage)](https://www.stackage.org/package/splint)
-
-:warning: This package is not maintained anymore.
+[![CI](https://github.com/tfausak/splint/actions/workflows/ci.yml/badge.svg)](https://github.com/tfausak/splint/actions/workflows/ci.yml)
+[![Hackage](https://badgen.net/hackage/v/splint)](https://hackage.haskell.org/package/splint)
 
 Splint makes [HLint 3][] available as a [GHC source plugin][]. It is similar to
 [hlint-source-plugin][] by Ollie Charles, except that it doesn't have to
@@ -31,7 +28,7 @@
 ```
 Main.hs:1:8: warning:
     Use concatMap
-    Perhaps: print (concatMap pure ['a' .. 'z'])
+    Suggested fix: print (concatMap pure ['a' .. 'z'])
   |
 1 | main = print . concat $ map pure [ 'a' .. 'z' ]
   |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/source/library/Splint.hs b/source/library/Splint.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Splint.hs
@@ -0,0 +1,73 @@
+module Splint where
+
+import qualified Data.Maybe as Maybe
+import qualified GHC.Data.Bag
+import qualified GHC.Driver.Config.Diagnostic
+import qualified GHC.Driver.Errors
+import qualified GHC.Driver.Errors.Types
+import qualified GHC.Hs
+import qualified GHC.Plugins
+import qualified GHC.Types.Error
+import qualified GHC.Utils.Error
+import qualified GHC.Utils.Logger
+import qualified Language.Haskell.HLint as HLint
+import qualified Splint.Replacement as Replacement
+import qualified Splint.Settings as Settings
+
+plugin :: GHC.Plugins.Plugin
+plugin =
+  GHC.Plugins.defaultPlugin
+    { GHC.Plugins.parsedResultAction = parsedResultAction,
+      GHC.Plugins.pluginRecompile = GHC.Plugins.purePlugin
+    }
+
+parsedResultAction ::
+  [GHC.Plugins.CommandLineOption] ->
+  modSummary ->
+  GHC.Plugins.ParsedResult ->
+  GHC.Plugins.Hsc GHC.Plugins.ParsedResult
+parsedResultAction commandLineOptions _modSummary parsedResult = do
+  logger <- GHC.Utils.Logger.getLogger
+  dynFlags <- GHC.Plugins.getDynFlags
+  let ghcMessageOpts = GHC.Driver.Config.Diagnostic.initPrintConfig dynFlags
+      diagOpts = GHC.Driver.Config.Diagnostic.initDiagOpts dynFlags
+  GHC.Plugins.liftIO $ do
+    settings <- Settings.load commandLineOptions
+    GHC.Driver.Errors.printOrThrowDiagnostics logger ghcMessageOpts diagOpts
+      . GHC.Types.Error.mkMessages
+      . GHC.Data.Bag.listToBag
+      . fmap (ideaToWarnMsg diagOpts)
+      . uncurry HLint.applyHints settings
+      . pure
+      . HLint.createModuleEx
+      . GHC.Hs.hpm_module
+      $ GHC.Plugins.parsedResultModule parsedResult
+  pure parsedResult
+
+ideaToWarnMsg :: GHC.Utils.Error.DiagOpts -> HLint.Idea -> GHC.Driver.Errors.Types.WarnMsg
+ideaToWarnMsg diagOpts idea =
+  let srcSpan = HLint.ideaSpan idea
+      ghcHints =
+        fmap (GHC.Types.Error.UnknownHint . Replacement.fromString)
+          . Maybe.maybeToList
+          $ HLint.ideaTo idea
+      decoratedSDoc =
+        GHC.Types.Error.mkDecorated $
+          GHC.Plugins.text
+            (HLint.ideaHint idea)
+            : fmap
+              (GHC.Plugins.text . mappend "Note: " . show)
+              (HLint.ideaNote idea)
+      diagnosticReason = case HLint.ideaSeverity idea of
+        HLint.Ignore -> GHC.Types.Error.WarningWithoutFlag
+        HLint.Suggestion -> GHC.Types.Error.WarningWithoutFlag
+        HLint.Warning -> GHC.Types.Error.WarningWithoutFlag
+        HLint.Error -> GHC.Types.Error.ErrorWithoutFlag
+      diagnosticMessage =
+        GHC.Types.Error.DiagnosticMessage
+          { GHC.Types.Error.diagHints = ghcHints,
+            GHC.Types.Error.diagMessage = decoratedSDoc,
+            GHC.Types.Error.diagReason = diagnosticReason
+          }
+      ghcMessage = GHC.Driver.Errors.Types.ghcUnknownMessage diagnosticMessage
+   in GHC.Utils.Error.mkPlainMsgEnvelope diagOpts srcSpan ghcMessage
diff --git a/source/library/Splint/RemoteData.hs b/source/library/Splint/RemoteData.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Splint/RemoteData.hs
@@ -0,0 +1,8 @@
+module Splint.RemoteData where
+
+data RemoteData e a
+  = NotAsked
+  | Loading
+  | Failure e
+  | Success a
+  deriving (Eq, Show)
diff --git a/source/library/Splint/Replacement.hs b/source/library/Splint/Replacement.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Splint/Replacement.hs
@@ -0,0 +1,16 @@
+module Splint.Replacement where
+
+import qualified GHC.Plugins as Plugin
+
+newtype Replacement
+  = Replacement String
+  deriving (Eq, Show)
+
+instance Plugin.Outputable Replacement where
+  ppr = Plugin.text . toString
+
+fromString :: String -> Replacement
+fromString = Replacement
+
+toString :: Replacement -> String
+toString (Replacement x) = x
diff --git a/source/library/Splint/Settings.hs b/source/library/Splint/Settings.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Splint/Settings.hs
@@ -0,0 +1,71 @@
+module Splint.Settings where
+
+import qualified Control.Concurrent.STM as Stm
+import qualified Control.Exception as Exception
+import qualified Data.Map as Map
+import qualified Language.Haskell.HLint as HLint
+import qualified Splint.RemoteData as RemoteData
+import qualified System.IO.Unsafe as Unsafe
+
+type Settings = ([HLint.Classify], HLint.Hint)
+
+-- | Getting settings is not instantaneous. Since settings are usually reused
+-- between modules, it makes sense to cache them. However each module can
+-- potentially customize its settings using the @OPTIONS_GHC@ pragma. This
+-- variable is used as a cache of settings keyed on the command line options.
+cache ::
+  Stm.TVar
+    (Map.Map [String] (RemoteData.RemoteData Exception.IOException Settings))
+cache = Unsafe.unsafePerformIO $ Stm.newTVarIO Map.empty
+{-# NOINLINE cache #-}
+
+-- | Even though we cache settings based on command line options, we only want
+-- to load settings one at a time. Practically this is to work around a bug in
+-- GHC. But aside from that, loading settings calls @withArgs@ and doing that
+-- simultaneously in separate threads is dubious.
+-- <https://gitlab.haskell.org/ghc/ghc/issues/18261>
+semaphore :: Stm.TMVar ()
+semaphore = Unsafe.unsafePerformIO $ Stm.newTMVarIO ()
+{-# NOINLINE semaphore #-}
+
+withTMVar :: Stm.TMVar a -> (a -> IO b) -> IO b
+withTMVar x =
+  Exception.bracket
+    (Stm.atomically $ Stm.takeTMVar x)
+    (Stm.atomically . Stm.putTMVar x)
+
+load :: [String] -> IO Settings
+load commandLineOptions = do
+  remoteData <- Stm.atomically $ do
+    settings <- Stm.readTVar cache
+    let remoteData =
+          Map.findWithDefault RemoteData.NotAsked commandLineOptions settings
+    case remoteData of
+      RemoteData.NotAsked ->
+        Stm.modifyTVar cache $ Map.insert commandLineOptions RemoteData.Loading
+      RemoteData.Loading -> Stm.retry
+      _ -> pure ()
+    pure remoteData
+  case remoteData of
+    RemoteData.NotAsked -> do
+      result <-
+        withTMVar semaphore
+          . const
+          . Exception.try
+          $ HLint.argsSettings commandLineOptions
+      case result of
+        Left ioException -> do
+          Stm.atomically
+            . Stm.modifyTVar cache
+            . Map.insert commandLineOptions
+            $ RemoteData.Failure ioException
+          Exception.throwIO ioException
+        Right (_, classifies, hint) -> do
+          Stm.atomically
+            . Stm.modifyTVar cache
+            . Map.insert commandLineOptions
+            $ RemoteData.Success (classifies, hint)
+          pure (classifies, hint)
+    RemoteData.Loading -> load commandLineOptions
+    RemoteData.Failure ioException -> Exception.throwIO ioException
+    RemoteData.Success settings -> pure settings
diff --git a/splint.cabal b/splint.cabal
--- a/splint.cabal
+++ b/splint.cabal
@@ -1,12 +1,8 @@
-cabal-version: >= 1.10
-
+cabal-version: 2.2
 name: splint
-version: 1.0.2.1
-
+version: 2.0.0.0
 synopsis: HLint as a GHC source plugin.
 description:
-  Warning: This package is not maintained anymore.
-  .
   Splint makes HLint available as a GHC source plugin. To use it, pass
   @-fplugin=Splint@ to GHC. Any options passed to Splint are passed through to
   HLint. For example you can use @-fplugin-opt=Splint:'--ignore=Use concatMap'@
@@ -14,8 +10,8 @@
 
 build-type: Simple
 category: Development
-extra-source-files: README.markdown
-license-file: LICENSE.markdown
+extra-doc-files: README.markdown
+license-file: LICENSE.txt
 license: ISC
 maintainer: Taylor Fausak
 
@@ -23,39 +19,46 @@
   location: https://github.com/tfausak/splint
   type: git
 
+flag pedantic
+  default: False
+  manual: True
+
 library
   build-depends:
-    base >= 4.14 && < 4.17
-    , containers >= 0.6 && < 0.7
-    , ghc >= 8.10 && < 8.11 || >= 9.0 && < 9.3
-    , hlint
-    , stm >= 2.5 && < 2.6
+    base ^>=4.18.0.0 || ^>=4.19.0.0,
+    containers ^>=0.6.7,
+    ghc ^>=9.6.1 || ^>=9.8.1,
+    stm ^>=2.5.1.0,
+
   default-language: Haskell2010
+  -- cabal-gild: discover source/library
   exposed-modules:
     Splint
     Splint.RemoteData
+    Splint.Replacement
     Splint.Settings
+
   ghc-options:
     -Weverything
+    -Wno-all-missed-specialisations
     -Wno-implicit-prelude
     -Wno-missing-deriving-strategies
     -Wno-missing-export-lists
     -Wno-missing-exported-signatures
     -Wno-missing-import-lists
+    -Wno-missing-kind-signatures
     -Wno-missing-safe-haskell-mode
     -Wno-prepositive-qualified-module
     -Wno-safe
     -Wno-unsafe
-  hs-source-dirs: src/lib
 
-  if impl(ghc >= 9.2)
-    build-depends: hlint >= 3.4 && < 3.5
-    ghc-options: -Wno-missing-kind-signatures
-    hs-source-dirs: src/ghc-9.2
-  else
-    if impl(ghc >= 9.0)
-      build-depends: hlint >= 3.3 && < 3.4
-      hs-source-dirs: src/ghc-9.0
-    else
-      build-depends: hlint >= 3.2 && < 3.3
-      hs-source-dirs: src/ghc-8.10
+  hs-source-dirs: source/library
+
+  if flag(pedantic)
+    ghc-options: -Werror
+
+  if impl(ghc ^>=9.6.1)
+    build-depends: hlint ^>=3.6.1
+  elif impl(ghc ^>=9.8.1)
+    build-depends: hlint ^>=3.8
+    ghc-options: -Wno-missing-role-annotations
diff --git a/src/ghc-8.10/Splint.hs b/src/ghc-8.10/Splint.hs
deleted file mode 100644
--- a/src/ghc-8.10/Splint.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-module Splint where
-
-import qualified Bag as GHC
-import qualified ErrUtils as GHC
-import qualified GhcPlugins as GHC
-import qualified Language.Haskell.HLint as HLint
-import qualified Splint.Settings as Settings
-
-plugin :: GHC.Plugin
-plugin = GHC.defaultPlugin
-  { GHC.parsedResultAction = action
-  , GHC.pluginRecompile = GHC.purePlugin
-  }
-
-action
-  :: [GHC.CommandLineOption]
-  -> GHC.ModSummary
-  -> GHC.HsParsedModule
-  -> GHC.Hsc GHC.HsParsedModule
-action commandLineOptions modSummary hsParsedModule = do
-  (parseFlags, classifies, hint) <- GHC.liftIO $ Settings.load commandLineOptions
-  moduleEx <- parse parseFlags modSummary hsParsedModule
-  dynFlags <- GHC.getDynFlags
-  GHC.liftIO
-    . GHC.printOrThrowWarnings dynFlags
-    . GHC.listToBag
-    . fmap (ideaToWarnMsg dynFlags)
-    . filter ((/= HLint.Ignore) . HLint.ideaSeverity)
-    $ HLint.applyHints classifies hint [moduleEx]
-  pure hsParsedModule
-
-ideaToWarnMsg :: GHC.DynFlags -> HLint.Idea -> GHC.WarnMsg
-ideaToWarnMsg dynFlags idea =
-  let
-    mkErrMsg = case HLint.ideaSeverity idea of
-      HLint.Error -> GHC.mkPlainErrMsg
-      _ -> GHC.mkPlainWarnMsg
-    srcSpan = case HLint.unpackSrcSpan $ HLint.ideaSpan idea of
-      Nothing -> GHC.noSrcSpan
-      Just (file, (startLine, startColumn), (endLine, endColumn)) ->
-        GHC.mkSrcSpan
-          (GHC.mkSrcLoc (GHC.mkFastString file) startLine startColumn)
-          (GHC.mkSrcLoc (GHC.mkFastString file) endLine endColumn)
-    msgDoc = ideaToMsgDoc idea
-  in mkErrMsg dynFlags srcSpan msgDoc
-
-ideaToMsgDoc :: HLint.Idea -> GHC.MsgDoc
-ideaToMsgDoc idea = GHC.vcat
-  [ GHC.text $ HLint.ideaHint idea
-  , case HLint.ideaTo idea of
-    Just to | not $ null to -> GHC.text $ "Perhaps: " <> to
-    _ -> GHC.empty
-  , GHC.vcat . fmap (GHC.text . mappend "Note: " . show) $ HLint.ideaNote idea
-  ]
-
-parse
-  :: HLint.ParseFlags
-  -> GHC.ModSummary
-  -> GHC.HsParsedModule
-  -> GHC.Hsc HLint.ModuleEx
-parse _ _ hsParsedModule = do
-  let
-    apiAnns = GHC.hpm_annotations hsParsedModule
-    hsModule = GHC.hpm_module hsParsedModule
-  pure $ HLint.createModuleEx apiAnns hsModule
diff --git a/src/ghc-9.0/Splint.hs b/src/ghc-9.0/Splint.hs
deleted file mode 100644
--- a/src/ghc-9.0/Splint.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-module Splint where
-
-import qualified GHC.Data.Bag as GHC
-import qualified GHC.Plugins as GHC
-import qualified GHC.Utils.Error as GHC
-import qualified Language.Haskell.HLint as HLint
-import qualified Splint.Settings as Settings
-
-plugin :: GHC.Plugin
-plugin = GHC.defaultPlugin
-  { GHC.parsedResultAction = action
-  , GHC.pluginRecompile = GHC.purePlugin
-  }
-
-action
-  :: [GHC.CommandLineOption]
-  -> GHC.ModSummary
-  -> GHC.HsParsedModule
-  -> GHC.Hsc GHC.HsParsedModule
-action commandLineOptions modSummary hsParsedModule = do
-  (parseFlags, classifies, hint) <- GHC.liftIO $ Settings.load commandLineOptions
-  moduleEx <- parse parseFlags modSummary hsParsedModule
-  dynFlags <- GHC.getDynFlags
-  GHC.liftIO
-    . GHC.printOrThrowWarnings dynFlags
-    . GHC.listToBag
-    . fmap (ideaToWarnMsg dynFlags)
-    . filter ((/= HLint.Ignore) . HLint.ideaSeverity)
-    $ HLint.applyHints classifies hint [moduleEx]
-  pure hsParsedModule
-
-ideaToWarnMsg :: GHC.DynFlags -> HLint.Idea -> GHC.WarnMsg
-ideaToWarnMsg dynFlags idea =
-  let
-    mkErrMsg = case HLint.ideaSeverity idea of
-      HLint.Error -> GHC.mkPlainErrMsg
-      _ -> GHC.mkPlainWarnMsg
-    srcSpan = case HLint.unpackSrcSpan $ HLint.ideaSpan idea of
-      Nothing -> GHC.noSrcSpan
-      Just (file, (startLine, startColumn), (endLine, endColumn)) ->
-        GHC.mkSrcSpan
-          (GHC.mkSrcLoc (GHC.mkFastString file) startLine startColumn)
-          (GHC.mkSrcLoc (GHC.mkFastString file) endLine endColumn)
-    msgDoc = ideaToMsgDoc idea
-  in mkErrMsg dynFlags srcSpan msgDoc
-
-ideaToMsgDoc :: HLint.Idea -> GHC.MsgDoc
-ideaToMsgDoc idea = GHC.vcat
-  [ GHC.text $ HLint.ideaHint idea
-  , case HLint.ideaTo idea of
-    Just to | not $ null to -> GHC.text $ "Perhaps: " <> to
-    _ -> GHC.empty
-  , GHC.vcat . fmap (GHC.text . mappend "Note: " . show) $ HLint.ideaNote idea
-  ]
-
-parse
-  :: HLint.ParseFlags
-  -> GHC.ModSummary
-  -> GHC.HsParsedModule
-  -> GHC.Hsc HLint.ModuleEx
-parse _ _ hsParsedModule = do
-  let
-    apiAnns = GHC.hpm_annotations hsParsedModule
-    hsModule = GHC.hpm_module hsParsedModule
-  pure $ HLint.createModuleEx apiAnns hsModule
diff --git a/src/ghc-9.2/Splint.hs b/src/ghc-9.2/Splint.hs
deleted file mode 100644
--- a/src/ghc-9.2/Splint.hs
+++ /dev/null
@@ -1,62 +0,0 @@
-module Splint where
-
-import qualified GHC.Data.Bag as Bag
-import qualified GHC.Driver.Errors as Errors
-import qualified GHC.Hs as Hs
-import qualified GHC.Plugins as Plugins
-import qualified GHC.Types.Error as Error
-import qualified GHC.Utils.Logger as Logger
-import qualified Language.Haskell.HLint as HLint
-import qualified Splint.Settings as Settings
-
-plugin :: Plugins.Plugin
-plugin = Plugins.defaultPlugin
-  { Plugins.parsedResultAction = parsedResultAction
-  , Plugins.pluginRecompile = Plugins.purePlugin
-  }
-
-parsedResultAction
-  :: [Plugins.CommandLineOption]
-  -> Plugins.ModSummary
-  -> Hs.HsParsedModule
-  -> Plugins.Hsc Hs.HsParsedModule
-parsedResultAction commandLineOptions _modSummary hsParsedModule = do
-  (_parseFlags, classifies, hint) <- Plugins.liftIO
-    $ Settings.load commandLineOptions
-  logger <- Logger.getLogger
-  dynFlags <- Plugins.getDynFlags
-  Plugins.liftIO
-    . Errors.printOrThrowWarnings logger dynFlags
-    . Bag.listToBag
-    . fmap ideaToWarnMsg
-    . filter ((/=) HLint.Ignore . HLint.ideaSeverity)
-    . HLint.applyHints classifies hint
-    . pure
-    . HLint.createModuleEx
-    $ Hs.hpm_module hsParsedModule
-  pure hsParsedModule
-
-ideaToWarnMsg :: HLint.Idea -> Error.WarnMsg
-ideaToWarnMsg idea =
-  Error.mkPlainWarnMsg (ideaToSrcSpan idea) (ideaToSDoc idea)
-
-ideaToSrcSpan :: HLint.Idea -> Plugins.SrcSpan
-ideaToSrcSpan idea = case HLint.unpackSrcSpan $ HLint.ideaSpan idea of
-  Nothing -> Plugins.noSrcSpan
-  Just (filePath, (startLine, startColumn), (endLine, endColumn)) ->
-    let fastString = Plugins.mkFastString filePath
-    in
-      Plugins.mkSrcSpan
-        (Plugins.mkSrcLoc fastString startLine startColumn)
-        (Plugins.mkSrcLoc fastString endLine endColumn)
-
-ideaToSDoc :: HLint.Idea -> Error.SDoc
-ideaToSDoc idea = Plugins.vcat
-  [ Plugins.text $ HLint.ideaHint idea
-  , case HLint.ideaTo idea of
-    Just to | not $ null to -> Plugins.text $ "Perhaps: " <> to
-    _ -> Plugins.empty
-  , Plugins.vcat
-  . fmap (Plugins.text . mappend "Note: " . show)
-  $ HLint.ideaNote idea
-  ]
diff --git a/src/lib/Splint/RemoteData.hs b/src/lib/Splint/RemoteData.hs
deleted file mode 100644
--- a/src/lib/Splint/RemoteData.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Splint.RemoteData where
-
-data RemoteData e a
-  = NotAsked
-  | Loading
-  | Failure e
-  | Success a
-  deriving (Eq, Show)
diff --git a/src/lib/Splint/Settings.hs b/src/lib/Splint/Settings.hs
deleted file mode 100644
--- a/src/lib/Splint/Settings.hs
+++ /dev/null
@@ -1,67 +0,0 @@
-module Splint.Settings where
-
-import qualified Control.Concurrent.STM as Stm
-import qualified Control.Exception as Exception
-import qualified Data.Map as Map
-import qualified Language.Haskell.HLint as HLint
-import qualified Splint.RemoteData as RemoteData
-import qualified System.IO.Unsafe as Unsafe
-
-type Settings = (HLint.ParseFlags, [HLint.Classify], HLint.Hint)
-
--- | Getting settings is not instantaneous. Since settings are usually reused
--- between modules, it makes sense to cache them. However each module can
--- potentially customize its settings using the @OPTIONS_GHC@ pragma. This
--- variable is used as a cache of settings keyed on the command line options.
-cache
-  :: Stm.TVar
-       (Map.Map [String] (RemoteData.RemoteData Exception.IOException Settings))
-cache = Unsafe.unsafePerformIO $ Stm.newTVarIO Map.empty
-{-# NOINLINE cache #-}
-
--- | Even though we cache settings based on command line options, we only want
--- to load settings one at a time. Practically this is to work around a bug in
--- GHC. But aside from that, loading settings calls @withArgs@ and doing that
--- simultaneously in separate threads is dubious.
--- <https://gitlab.haskell.org/ghc/ghc/issues/18261>
-semaphore :: Stm.TMVar ()
-semaphore = Unsafe.unsafePerformIO $ Stm.newTMVarIO ()
-{-# NOINLINE semaphore #-}
-
-withTMVar :: Stm.TMVar a -> (a -> IO b) -> IO b
-withTMVar x = Exception.bracket
-  (Stm.atomically $ Stm.takeTMVar x)
-  (Stm.atomically . Stm.putTMVar x)
-
-load :: [String] -> IO Settings
-load commandLineOptions = do
-  remoteData <- Stm.atomically $ do
-    settings <- Stm.readTVar cache
-    let
-      remoteData =
-        Map.findWithDefault RemoteData.NotAsked commandLineOptions settings
-    case remoteData of
-      RemoteData.NotAsked ->
-        Stm.modifyTVar cache $ Map.insert commandLineOptions RemoteData.Loading
-      RemoteData.Loading -> Stm.retry
-      _ -> pure ()
-    pure remoteData
-  case remoteData of
-    RemoteData.NotAsked -> withTMVar semaphore . const $ do
-      result <- Exception.try $ HLint.argsSettings commandLineOptions
-      case result of
-        Left ioException -> do
-          Stm.atomically
-            . Stm.modifyTVar cache
-            . Map.insert commandLineOptions
-            $ RemoteData.Failure ioException
-          Exception.throwIO ioException
-        Right settings -> do
-          Stm.atomically
-            . Stm.modifyTVar cache
-            . Map.insert commandLineOptions
-            $ RemoteData.Success settings
-          pure settings
-    RemoteData.Loading -> load commandLineOptions
-    RemoteData.Failure ioException -> Exception.throwIO ioException
-    RemoteData.Success settings -> pure settings
