diff --git a/hints.md b/hints.md
index 09be1475..a9ec7307 100644
--- a/hints.md
+++ b/hints.md
@@ -1176,6 +1176,38 @@ Does not support refactoring.
+## Builtin NoCapitalisms
+
+
+
+| Hint Name |
+Hint |
+Severity |
+
+
+| Avoid capitalisms |
+
+Example:
+
+getFOO = _
+
+
+Found:
+
+getFOO = ...
+
+
+Suggestion:
+
+
+
+
+Does not support refactoring.
+ |
+Ignore |
+
+
+
## Builtin NumLiteral
diff --git a/hlint.cabal b/hlint.cabal
index c3c8e2d4..81451df6 100644
--- a/hlint.cabal
+++ b/hlint.cabal
@@ -158,16 +158,18 @@ library
Hint.ListRec
Hint.Match
Hint.Monad
+ Hint.NameHelpers
Hint.Naming
Hint.Negation
Hint.NewType
+ Hint.NoCapitalisms
+ Hint.NumLiteral
Hint.Pattern
Hint.Pragma
Hint.Restrict
Hint.Smell
Hint.Type
Hint.Unsafe
- Hint.NumLiteral
Test.All
Test.Annotations
Test.InputOutput
diff --git a/src/Hint/All.hs b/src/Hint/All.hs
index 41665c8e..0af41128 100644
--- a/src/Hint/All.hs
+++ b/src/Hint/All.hs
@@ -34,6 +34,7 @@ import Hint.Unsafe
import Hint.NewType
import Hint.Smell
import Hint.NumLiteral
+import Hint.NoCapitalisms
-- | A list of the builtin hints wired into HLint.
-- This list is likely to grow over time.
@@ -41,7 +42,7 @@ data HintBuiltin =
HintList | HintListRec | HintMonad | HintLambda | HintFixities | HintNegation |
HintBracket | HintNaming | HintPattern | HintImport | HintExport |
HintPragma | HintExtensions | HintUnsafe | HintDuplicate | HintRestrict |
- HintComment | HintNewType | HintSmell | HintNumLiteral
+ HintComment | HintNewType | HintSmell | HintNumLiteral | HintNoCapitalisms
deriving (Show,Eq,Ord,Bounded,Enum)
-- See https://github.com/ndmitchell/hlint/issues/1150 - Duplicate is too slow
@@ -50,26 +51,27 @@ issue1150 = True
builtin :: HintBuiltin -> Hint
builtin x = case x of
- HintLambda -> decl lambdaHint
- HintImport -> modu importHint
- HintExport -> modu exportHint
- HintComment -> modu commentHint
- HintPragma -> modu pragmaHint
- HintDuplicate -> if issue1150 then mempty else mods duplicateHint
- HintRestrict -> mempty{hintModule=restrictHint}
- HintList -> decl listHint
- HintNewType -> decl newtypeHint
- HintUnsafe -> decl unsafeHint
- HintListRec -> decl listRecHint
- HintNaming -> decl namingHint
- HintBracket -> decl bracketHint
- HintFixities -> mempty{hintDecl=fixitiesHint}
- HintNegation -> decl negationParensHint
- HintSmell -> mempty{hintDecl=smellHint,hintModule=smellModuleHint}
- HintPattern -> decl patternHint
- HintMonad -> decl monadHint
- HintExtensions -> modu extensionsHint
- HintNumLiteral -> decl numLiteralHint
+ HintLambda -> decl lambdaHint
+ HintImport -> modu importHint
+ HintExport -> modu exportHint
+ HintComment -> modu commentHint
+ HintPragma -> modu pragmaHint
+ HintDuplicate -> if issue1150 then mempty else mods duplicateHint
+ HintRestrict -> mempty{hintModule=restrictHint}
+ HintList -> decl listHint
+ HintNewType -> decl newtypeHint
+ HintUnsafe -> decl unsafeHint
+ HintListRec -> decl listRecHint
+ HintNaming -> decl namingHint
+ HintBracket -> decl bracketHint
+ HintFixities -> mempty{hintDecl=fixitiesHint}
+ HintNegation -> decl negationParensHint
+ HintSmell -> mempty{hintDecl=smellHint,hintModule=smellModuleHint}
+ HintPattern -> decl patternHint
+ HintMonad -> decl monadHint
+ HintExtensions -> modu extensionsHint
+ HintNumLiteral -> decl numLiteralHint
+ HintNoCapitalisms -> decl noCapitalismsHint
where
wrap = timed "Hint" (drop 4 $ show x) . forceList
decl f = mempty{hintDecl=const $ \a b c -> wrap $ f a b c}
diff --git a/src/Hint/NameHelpers.hs b/src/Hint/NameHelpers.hs
new file mode 100644
index 00000000..9685ec07
--- /dev/null
+++ b/src/Hint/NameHelpers.hs
@@ -0,0 +1,52 @@
+module Hint.NameHelpers where
+
+import Data.List.Extra as E
+import Data.List.NonEmpty as NE
+import Data.Maybe
+
+import GHC.Types.Basic
+import GHC.Types.SourceText
+import GHC.Data.FastString
+import GHC.Hs.Decls
+import GHC.Hs.Extension
+import GHC.Hs
+import GHC.Types.SrcLoc
+
+import Language.Haskell.GhclibParserEx.GHC.Utils.Outputable
+import GHC.Util
+
+-- | Replace RHSs of top-level value declarations with an ellipsis
+shorten :: LHsDecl GhcPs -> LHsDecl GhcPs
+shorten (L locDecl (ValD ttg0 bind@(FunBind _ _ matchGroup@(MG FromSource (L locMatches matches))))) =
+ L locDecl (ValD ttg0 bind {fun_matches = matchGroup {mg_alts = L locMatches $ E.map shortenMatch matches}})
+shorten (L locDecl (ValD ttg0 bind@(PatBind _ _ _ grhss@(GRHSs _ rhss _)))) =
+ L locDecl (ValD ttg0 bind {pat_rhs = grhss {grhssGRHSs = E.map shortenLGRHS rhss}})
+shorten x = x
+
+shortenMatch :: LMatch GhcPs (LHsExpr GhcPs) -> LMatch GhcPs (LHsExpr GhcPs)
+shortenMatch (L locMatch match@(Match _ _ _ grhss@(GRHSs _ rhss _))) =
+ L locMatch match {m_grhss = grhss {grhssGRHSs = E.map shortenLGRHS rhss}}
+
+shortenLGRHS :: LGRHS GhcPs (LHsExpr GhcPs) -> LGRHS GhcPs (LHsExpr GhcPs)
+shortenLGRHS (L locGRHS (GRHS ttg0 guards (L locExpr _))) =
+ L locGRHS (GRHS ttg0 guards (L locExpr dots))
+ where
+ dots :: HsExpr GhcPs
+ dots = HsLit noExtField (HsString (SourceText (fsLit "...")) (fsLit "..."))
+
+-- | Get the names from all top-level declarations including constructor names
+getNames :: LHsDecl GhcPs -> [String]
+getNames decl = maybeToList (declName decl) ++ getConstructorNames (unLoc decl)
+
+getConstructorNames :: HsDecl GhcPs -> [String]
+getConstructorNames tycld = case tycld of
+ (TyClD _ (DataDecl _ _ _ _ (HsDataDefn _ _ _ _ (NewTypeCon con) _))) -> conNames [con]
+ (TyClD _ (DataDecl _ _ _ _ (HsDataDefn _ _ _ _ (DataTypeCons _ cons) _))) -> conNames cons
+ _ -> []
+ where
+ conNames :: [LConDecl GhcPs] -> [String]
+ conNames = concatMap (E.map unsafePrettyPrint . conNamesInDecl . unLoc)
+
+ conNamesInDecl :: ConDecl GhcPs -> [LIdP GhcPs]
+ conNamesInDecl ConDeclH98 {con_name = name} = [name]
+ conNamesInDecl ConDeclGADT {con_names = names} = NE.toList names
diff --git a/src/Hint/Naming.hs b/src/Hint/Naming.hs
index 78d0d7e6..3d57ce72 100644
--- a/src/Hint/Naming.hs
+++ b/src/Hint/Naming.hs
@@ -42,18 +42,15 @@ foreign import ccall hexml_node_child :: IO ()
module Hint.Naming(namingHint) where
+import Hint.NameHelpers
import Hint.Type (Idea,DeclHint,suggest,ghcModule)
import Data.Generics.Uniplate.DataOnly
import Data.List.Extra (nubOrd, isPrefixOf)
-import Data.List.NonEmpty (toList)
import Data.Data
import Data.Char
import Data.Maybe
import Data.Set qualified as Set
-import GHC.Types.Basic
-import GHC.Types.SourceText
-import GHC.Data.FastString
import GHC.Hs.Decls
import GHC.Hs.Extension
import GHC.Hs
@@ -62,7 +59,6 @@ import GHC.Types.SrcLoc
import Language.Haskell.GhclibParserEx.GHC.Hs.Decls
import Language.Haskell.GhclibParserEx.GHC.Utils.Outputable
-import GHC.Util
namingHint :: DeclHint
namingHint _ modu = naming $ Set.fromList $ concatMap getNames $ hsmodDecls $ unLoc (ghcModule modu)
@@ -86,40 +82,6 @@ naming seen originalDecl =
]
replacedDecl = replaceNames suggestedNames originalDecl
-shorten :: LHsDecl GhcPs -> LHsDecl GhcPs
-shorten (L locDecl (ValD ttg0 bind@(FunBind _ _ matchGroup@(MG FromSource (L locMatches matches))))) =
- L locDecl (ValD ttg0 bind {fun_matches = matchGroup {mg_alts = L locMatches $ map shortenMatch matches}})
-shorten (L locDecl (ValD ttg0 bind@(PatBind _ _ _ grhss@(GRHSs _ rhss _)))) =
- L locDecl (ValD ttg0 bind {pat_rhs = grhss {grhssGRHSs = map shortenLGRHS rhss}})
-shorten x = x
-
-shortenMatch :: LMatch GhcPs (LHsExpr GhcPs) -> LMatch GhcPs (LHsExpr GhcPs)
-shortenMatch (L locMatch match@(Match _ _ _ grhss@(GRHSs _ rhss _))) =
- L locMatch match {m_grhss = grhss {grhssGRHSs = map shortenLGRHS rhss}}
-
-shortenLGRHS :: LGRHS GhcPs (LHsExpr GhcPs) -> LGRHS GhcPs (LHsExpr GhcPs)
-shortenLGRHS (L locGRHS (GRHS ttg0 guards (L locExpr _))) =
- L locGRHS (GRHS ttg0 guards (L locExpr dots))
- where
- dots :: HsExpr GhcPs
- dots = HsLit noExtField (HsString (SourceText (fsLit "...")) (fsLit "..."))
-
-getNames :: LHsDecl GhcPs -> [String]
-getNames decl = maybeToList (declName decl) ++ getConstructorNames (unLoc decl)
-
-getConstructorNames :: HsDecl GhcPs -> [String]
-getConstructorNames tycld = case tycld of
- (TyClD _ (DataDecl _ _ _ _ (HsDataDefn _ _ _ _ (NewTypeCon con) _))) -> conNames [con]
- (TyClD _ (DataDecl _ _ _ _ (HsDataDefn _ _ _ _ (DataTypeCons _ cons) _))) -> conNames cons
- _ -> []
- where
- conNames :: [LConDecl GhcPs] -> [String]
- conNames = concatMap (map unsafePrettyPrint . conNamesInDecl . unLoc)
-
- conNamesInDecl :: ConDecl GhcPs -> [LIdP GhcPs]
- conNamesInDecl ConDeclH98 {con_name = name} = [name]
- conNamesInDecl ConDeclGADT {con_names = names} = Data.List.NonEmpty.toList names
-
isSym :: String -> Bool
isSym (x:_) = not $ isAlpha x || x `elem` "_'"
isSym _ = False
diff --git a/src/Hint/NoCapitalisms.hs b/src/Hint/NoCapitalisms.hs
new file mode 100644
index 00000000..3c31c77d
--- /dev/null
+++ b/src/Hint/NoCapitalisms.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-
+ Detect uses of capitalisms
+
+ Do not allow two consecutive capital letters in top level
+ identifiers of types, classes, values and constructors.
+
+ Identifiers containing underscores are exempted from thus rule.
+ Identifiers of FFI bindings are exempted from thus rule.
+
+ Locally bound identifiers, field names and module names are not
+ checked.
+
+
+data Foo = MkFoo { getID :: String }
+data IO -- @Ignore
+data PersonID = P -- @Ignore
+sendIO :: IO () -- @Ignore
+sendIO = _ -- @Ignore
+class HasIO where -- @Ignore
+data Foo = FO -- @Ignore
+data LHsDecl -- @Ignore
+class FOO a where -- @Ignore
+class Foo a where getFOO :: Bool
+data Foo = Bar | BAAZ -- @Ignore
+data Foo = B_ar | BAAZ -- @Ignore
+data Foo = Bar | B_AAZ
+data OTPToken = OTPToken -- @Ignore
+data OTP_Token = Foo
+sendSMS = _ -- @Ignore
+runTLS = _ -- @Ignore
+runTLSSocket = _ -- @Ignore
+runTLS_Socket
+newtype TLSSettings = TLSSettings -- @Ignore
+tlsSettings
+data CertSettings = CertSettings
+tlsServerHooks
+tlsServerDHEParams = _ -- @Ignore
+type WarpTLSException = () -- @Ignore
+get_SMS
+runCI
+foreign import ccall _FIREMISSLES :: IO ()
+getSMS :: IO () -- @Ignore
+gFOO = _ -- @Ignore
+geFOO = _ -- @Ignore
+getFOO = _ -- @Ignore
+
+-}
+
+module Hint.NoCapitalisms(noCapitalismsHint) where
+
+import Hint.Type
+import Hint.NameHelpers
+import Data.List.Extra as E
+import Data.Char
+
+import GHC.Hs
+
+import Language.Haskell.GhclibParserEx.GHC.Hs.Decls
+
+noCapitalismsHint :: DeclHint
+noCapitalismsHint _ _ decl = [ remark Ignore "Avoid capitalisms" (reLoc (shorten decl))
+ | not $ isForD decl
+ , name <- nubOrd $ getNames decl
+ , not $ hasUnderscore name
+ , hasCapitalism name
+ ]
+
+hasUnderscore :: String -> Bool
+hasUnderscore = elem '_'
+
+hasCapitalism :: String -> Bool
+hasCapitalism s = any isAllUpper (bigrams s)
+ where
+ isAllUpper = all isUpper
+
+bigrams :: String -> [String]
+bigrams = \case
+ a:b:as -> [a,b] : bigrams (b:as)
+ _otherwise -> []
+
+
diff --git a/src/Idea.hs b/src/Idea.hs
index 896e7e5a..1a099290 100644
--- a/src/Idea.hs
+++ b/src/Idea.hs
@@ -3,7 +3,7 @@
module Idea(
Idea(..),
- rawIdea, idea, suggest, suggestRemove, ideaRemove, warn, ignore,
+ rawIdea, idea, suggest, suggestRemove, ideaRemove, warn, ignore, remark,
rawIdeaN, suggestN, ignoreNoSuggestion,
showIdeasJson, showIdeaANSI,
ideaFile,
@@ -107,6 +107,10 @@ idea severity hint from to =
ideaRemove :: Severity -> String -> SrcSpan -> String -> [Refactoring R.SrcSpan] -> Idea
ideaRemove severity hint span from = rawIdea severity hint span from (Just "") []
+remark :: GHC.Utils.Outputable.Outputable a
+ => Severity -> String -> Located a -> Idea
+remark severity hint from = rawIdeaN severity hint (getLoc from) (unsafePrettyPrint from) Nothing []
+
suggest :: (GHC.Utils.Outputable.Outputable a, GHC.Utils.Outputable.Outputable b) =>
String -> Located a -> Located b -> [Refactoring R.SrcSpan] -> Idea
suggest = idea Suggestion