Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions hints.md
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,38 @@ Does not support refactoring.
</tr>
</table>

## Builtin NoCapitalisms

<table>
<tr>
<th>Hint Name</th>
<th>Hint</th>
<th>Severity</th>
</tr>
<tr>
<td>Avoid capitalisms</td>
<td>
Example:
<code>
getFOO = _
</code>
<br>
Found:
<code>
getFOO = ...
</code>
<br>
Suggestion:
<code>

</code>
<br>
Does not support refactoring.
</td>
<td>Ignore</td>
</tr>
</table>

## Builtin NumLiteral

<table>
Expand Down
4 changes: 3 additions & 1 deletion hlint.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 23 additions & 21 deletions src/Hint/All.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ 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.
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
Expand All @@ -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}
Expand Down
52 changes: 52 additions & 0 deletions src/Hint/NameHelpers.hs
Original file line number Diff line number Diff line change
@@ -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
40 changes: 1 addition & 39 deletions src/Hint/Naming.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
83 changes: 83 additions & 0 deletions src/Hint/NoCapitalisms.hs
Original file line number Diff line number Diff line change
@@ -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.

<TEST>
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
</TEST>
-}

module Hint.NoCapitalisms(noCapitalismsHint) where
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"capitalism"? How about "NoCAPs"?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capitalism, as in, an expression written in all caps.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see such a meaning for capitalism in the dictionary.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, "NoCAPs" seems better since it doesn't have political connotations and shouldn't provoke unnecessary discussions, unlike the current name.


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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a Note to the hint, explaining what it does?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the description at beginning of the file sufficient? I followed the style of Hint.Naming.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's just a code comment. A Note is attached to a hint and is displayed with the hint. Alternatively, you can make the hint name more descriptive: "Avoid three consecutive capital letters" instead of "Avoid capitalisms".

| 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 -> []


6 changes: 5 additions & 1 deletion src/Idea.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down