Skip to content

Commit eb2e7d4

Browse files
committed
Add typeApplications option for restricted functions
1 parent 514ad8f commit eb2e7d4

File tree

6 files changed

+176
-11
lines changed

6 files changed

+176
-11
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,16 @@ This:
430430
* Requires that `Unsafe` must always be imported qualified, and can't be aliased.
431431
* Forbids `import qualified Prelude` and `import Prelude qualified` (with or without explicit import list).
432432

433+
For restricted functions, you can also control visible type applications with `typeApplications` set to either `'required'` or `'forbidden'`:
434+
435+
```yaml
436+
- functions:
437+
- {name: fromIntegral, typeApplications: required}
438+
- {name: show, typeApplications: required}
439+
```
440+
441+
This flags any call to `fromIntegral` or `show` that omits visible type arguments.
442+
433443
You can match on module names using [glob](https://en.wikipedia.org/wiki/Glob_(programming))-style wildcards. Module names are treated like file paths, except that periods in module names are like directory separators in file paths. So `**.*Spec` will match `Spec`, `PreludeSpec`, `Data.ListSpec`, and many more. But `*Spec` won't match `Data.ListSpec` because of the separator. See [the filepattern library](https://hackage.haskell.org/package/filepattern) for a more thorough description of the matching.
434444

435445
Restrictions are unified between wildcard and specific matches. With `asRequired`, `importStyle` and `qualifiedStyle` fields, the more specific option takes precedence. The list fields are merged. With multiple wildcard matches, the precedence between them is not guaranteed (but in practice, names are sorted in the reverse lexicograpic order, and the first one wins -- which hopefully means the more specific one more often than not)

data/type_applications.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- functions:
2+
- {name: fromIntegral, typeApplications: required}
3+
- {name: Just, typeApplications: required}
4+
- {name: id, typeApplications: forbidden}
5+
- {name: Left, typeApplications: forbidden}

src/Config/Type.hs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
module Config.Type(
1010
Severity(..), Classify(..), HintRule(..), Note(..), Setting(..),
1111
Restrict(..), RestrictType(..), RestrictIdents(..), SmellType(..),
12-
RestrictImportStyle(..), QualifiedStyle(..),
12+
RestrictImportStyle(..), QualifiedStyle(..), RestrictTypeApp(..),
1313
defaultHintName, isUnifyVar, showNotes, getSeverity, getRestrictType, getSmellType
1414
) where
1515

@@ -147,6 +147,16 @@ data RestrictImportStyle
147147
| ImportStyleUnrestricted
148148
deriving Show
149149

150+
data RestrictTypeApp
151+
= TypeAppRequired
152+
| TypeAppForbidden
153+
deriving (Eq, Show)
154+
155+
instance Semigroup RestrictTypeApp where
156+
TypeAppRequired <> TypeAppRequired = TypeAppRequired
157+
TypeAppForbidden <> TypeAppForbidden = TypeAppForbidden
158+
x <> y = error $ "Incompatible type application restrictions: " ++ show (x, y)
159+
150160
data QualifiedStyle
151161
= QualifiedStylePre
152162
| QualifiedStylePost
@@ -161,6 +171,7 @@ data Restrict = Restrict
161171
,restrictAsRequired :: Alt Maybe Bool -- for RestrictModule only
162172
,restrictImportStyle :: Alt Maybe RestrictImportStyle -- for RestrictModule only
163173
,restrictQualifiedStyle :: Alt Maybe QualifiedStyle -- for RestrictModule only
174+
,restrictTypeApp :: Maybe RestrictTypeApp -- for RestrictFunction only
164175
,restrictWithin :: [(String, String)]
165176
,restrictIdents :: RestrictIdents -- for RestrictModule only, what identifiers can be imported from it
166177
,restrictMessage :: Maybe String

src/Config/Yaml.hs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,19 @@ parseRestrict restrictType v = do
343343
Just def -> do
344344
b <- parseBool def
345345
allowFields v ["default"]
346-
pure $ Restrict restrictType b [] mempty mempty mempty mempty [] NoRestrictIdents Nothing
346+
pure Restrict
347+
{ restrictType = restrictType
348+
, restrictDefault = b
349+
, restrictName = []
350+
, restrictAs = mempty
351+
, restrictAsRequired = mempty
352+
, restrictImportStyle = mempty
353+
, restrictQualifiedStyle = mempty
354+
, restrictTypeApp = Nothing
355+
, restrictWithin = []
356+
, restrictIdents = NoRestrictIdents
357+
, restrictMessage = Nothing
358+
}
347359
Nothing -> do
348360
restrictName <- parseFieldOpt "name" v >>= maybe (pure []) parseArrayString
349361
restrictWithin <- parseFieldOpt "within" v >>= maybe (pure [("","")]) (parseArray >=> concatMapM parseWithin)
@@ -361,6 +373,10 @@ parseRestrict restrictType v = do
361373
, ("post" , QualifiedStylePost)
362374
, ("unrestricted", QualifiedStyleUnrestricted)
363375
]
376+
restrictTypeApp <- parseFieldOpt "typeApplications" v >>= maybeParseEnum
377+
[ ("required" , TypeAppRequired)
378+
, ("forbidden", TypeAppForbidden)
379+
]
364380

365381

366382
restrictBadIdents <- parseFieldOpt "badidents" v
@@ -375,9 +391,10 @@ parseRestrict restrictType v = do
375391
restrictMessage <- parseFieldOpt "message" v >>= maybeParse parseString
376392
allowFields v $
377393
["name", "within", "message"] ++
378-
if restrictType == RestrictModule
379-
then ["as", "asRequired", "importStyle", "qualifiedStyle", "badidents", "only"]
380-
else []
394+
case restrictType of
395+
RestrictModule -> ["as", "asRequired", "importStyle", "qualifiedStyle", "badidents", "only"]
396+
RestrictFunction -> ["typeApplications"]
397+
_ -> []
381398
pure Restrict{restrictDefault=True,..}
382399

383400
parseWithin :: Val -> Parser [(String, String)] -- (module, decl)

src/Hint/Restrict.hs

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,21 @@ instance Semigroup RestrictItem where
8787
<> RestrictItem y1 y2 y3 y4 y5 y6 y7
8888
= RestrictItem (x1<>y1) (x2<>y2) (x3<>y3) (x4<>y4) (x5<>y5) (x6<>y6) (x7<>y7)
8989

90+
data RestrictFunctionItem = RestrictFunctionItem
91+
{rfiWithin :: [(String, String)]
92+
,rfiMessage :: Maybe String
93+
,rfiTypeApp :: Maybe RestrictTypeApp
94+
}
95+
96+
instance Semigroup RestrictFunctionItem where
97+
RestrictFunctionItem a1 a2 a3 <> RestrictFunctionItem b1 b2 b3 =
98+
RestrictFunctionItem (a1 <> b1) (a2 <> b2) (a3 <> b3)
99+
90100
-- Contains a map from module (Nothing if the rule is unqualified) to (within, message), so that we can
91101
-- distinguish functions with the same name.
92102
-- For example, this allows us to have separate rules for "Data.Map.fromList" and "Data.Set.fromList".
93103
-- Using newtype rather than type because we want to define (<>) as 'Map.unionWith (<>)'.
94-
newtype RestrictFunction = RestrictFun (Map.Map (Maybe String) ([(String, String)], Maybe String))
104+
newtype RestrictFunction = RestrictFun (Map.Map (Maybe String) RestrictFunctionItem)
95105

96106
instance Semigroup RestrictFunction where
97107
RestrictFun m1 <> RestrictFun m2 = RestrictFun (Map.unionWith (<>) m1 m2)
@@ -104,7 +114,11 @@ restrictions settings = (rFunction, rOthers)
104114
where
105115
(map snd -> rfs, ros) = partition ((== RestrictFunction) . fst) [(restrictType x, x) | SettingRestrict x <- settings]
106116
rFunction = (all restrictDefault rfs, Map.fromListWith (<>) [mkRf s r | r <- rfs, s <- restrictName r])
107-
mkRf s Restrict{..} = (name, RestrictFun $ Map.singleton modu (restrictWithin, restrictMessage))
117+
mkRf s Restrict{..} = (name, RestrictFun $ Map.singleton modu RestrictFunctionItem
118+
{ rfiWithin = restrictWithin
119+
, rfiMessage = restrictMessage
120+
, rfiTypeApp = restrictTypeApp
121+
})
108122
where
109123
-- Parse module and name from s. module = Nothing if the rule is unqualified.
110124
(modu, name) = first (fmap NonEmpty.init . NonEmpty.nonEmpty) (breakEnd (== '.') s)
@@ -271,14 +285,57 @@ importListToIdents =
271285

272286
checkFunctions :: Scope -> String -> [LHsDecl GhcPs] -> RestrictFunctions -> [Idea]
273287
checkFunctions scope modu decls (def, mp) =
274-
[ (ideaMessage message $ ideaNoTo $ warn "Avoid restricted function" (reLoc x) (reLoc x) []){ideaDecl = [dname]}
288+
[ (ideaMessage rfiMessage $ ideaNoTo $ warn hint (reLoc x) (reLoc x) []){ideaDecl = [dname]}
275289
| d <- decls
276290
, let dname = fromMaybe "" (declName d)
277291
, x <- universeBi d :: [LocatedN RdrName]
278292
, let xMods = possModules scope x
279-
, let (withins, message) = fromMaybe ([("","") | def], Nothing) (findFunction mp x xMods)
280-
, not $ within modu dname withins
293+
, let RestrictFunctionItem{..} = fromMaybe defaultRestrictFunction (findFunction mp x xMods)
294+
, let withinOk = within modu dname rfiWithin
295+
, let typeAppOk = maybe True (\req -> typeAppSatisfies req typeAppHeads (locA $ getLoc x)) rfiTypeApp
296+
, let hint = case () of
297+
_ | not withinOk -> "Avoid restricted function"
298+
| otherwise -> typeAppHint rfiTypeApp
299+
, not withinOk || not typeAppOk
281300
]
301+
where
302+
typeAppHeads = visibleTypeAppHeads decls
303+
defaultRestrictFunction = RestrictFunctionItem [("","") | def] Nothing Nothing
304+
305+
typeAppHint :: Maybe RestrictTypeApp -> String
306+
typeAppHint (Just TypeAppRequired) = "Use visible type application"
307+
typeAppHint (Just TypeAppForbidden) = "Avoid visible type application"
308+
typeAppHint Nothing = "Avoid restricted function"
309+
310+
typeAppSatisfies :: RestrictTypeApp -> Set.Set SrcSpanD -> SrcSpan -> Bool
311+
typeAppSatisfies TypeAppRequired heads = (`Set.member` heads) . SrcSpanD
312+
typeAppSatisfies TypeAppForbidden heads = (`Set.notMember` heads) . SrcSpanD
313+
314+
visibleTypeAppHeads :: [LHsDecl GhcPs] -> Set.Set SrcSpanD
315+
visibleTypeAppHeads decls =
316+
Set.fromList $ exprHeads ++ patHeads
317+
where
318+
exprHeads =
319+
[ SrcSpanD $ locA $ getLoc name
320+
| expr <- universeBi decls :: [LHsExpr GhcPs]
321+
, L _ (HsAppType _ fun _) <- [expr]
322+
, Just name <- [typeAppHead fun]
323+
]
324+
patHeads =
325+
[ SrcSpanD $ locA $ getLoc name
326+
| pat <- universeBi decls :: [LPat GhcPs]
327+
, L _ (ConPat _ name details) <- [pat]
328+
, hasTypeApp details
329+
]
330+
hasTypeApp (PrefixCon tyArgs _) = not $ null tyArgs
331+
hasTypeApp _ = False
332+
333+
typeAppHead :: LHsExpr GhcPs -> Maybe (LocatedN RdrName)
334+
typeAppHead (L _ (HsVar _ name)) = Just name
335+
typeAppHead (L _ (HsApp _ fun _)) = typeAppHead fun
336+
typeAppHead (L _ (HsAppType _ fun _)) = typeAppHead fun
337+
typeAppHead (L _ (HsPar _ fun)) = typeAppHead fun
338+
typeAppHead _ = Nothing
282339

283340
-- Returns Just iff there are rules for x, which are either unqualified, or qualified with a module that is
284341
-- one of x's possible modules.
@@ -288,7 +345,7 @@ findFunction
288345
:: Map.Map String RestrictFunction
289346
-> LocatedN RdrName
290347
-> [ModuleName]
291-
-> Maybe ([(String, String)], Maybe String)
348+
-> Maybe RestrictFunctionItem
292349
findFunction restrictMap (rdrNameStr -> x) (map moduleNameString -> possMods) = do
293350
(RestrictFun mp) <- Map.lookup x restrictMap
294351
n <- NonEmpty.nonEmpty . Map.elems $ Map.filterWithKey (const . maybe True (`elem` possMods)) mp

tests/type_applications.test

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---------------------------------------------------------------------
2+
RUN tests/typeAppsRequired.hs --hint=data/type_applications.yaml --only="Use visible type application"
3+
FILE tests/typeAppsRequired.hs
4+
{-# LANGUAGE TypeApplications #-}
5+
module TypeAppsRequired where
6+
7+
a = fromIntegral (1 :: Int)
8+
b = fromIntegral @Int @Integer (1 :: Int)
9+
OUTPUT
10+
tests/typeAppsRequired.hs:4:5-16: Warning: Use visible type application
11+
Found:
12+
fromIntegral
13+
Note: may break the code
14+
15+
1 hint
16+
17+
---------------------------------------------------------------------
18+
RUN tests/typeAppsRequiredPattern.hs --hint=data/type_applications.yaml --only="Use visible type application"
19+
FILE tests/typeAppsRequiredPattern.hs
20+
{-# LANGUAGE TypeApplications #-}
21+
module TypeAppsRequiredPattern where
22+
23+
f (Just x) = x
24+
g (Just @Int x) = x
25+
OUTPUT
26+
tests/typeAppsRequiredPattern.hs:4:4-7: Warning: Use visible type application
27+
Found:
28+
Just
29+
Note: may break the code
30+
31+
1 hint
32+
33+
---------------------------------------------------------------------
34+
RUN tests/typeAppsForbiddenPattern.hs --hint=data/type_applications.yaml --only="Avoid visible type application"
35+
FILE tests/typeAppsForbiddenPattern.hs
36+
{-# LANGUAGE TypeApplications #-}
37+
module TypeAppsForbiddenPattern where
38+
39+
f (Left @Int x) = x
40+
g (Left x) = x
41+
OUTPUT
42+
tests/typeAppsForbiddenPattern.hs:4:4-7: Warning: Avoid visible type application
43+
Found:
44+
Left
45+
Note: may break the code
46+
47+
1 hint
48+
49+
---------------------------------------------------------------------
50+
RUN tests/typeAppsForbidden.hs --hint=data/type_applications.yaml --only="Avoid visible type application"
51+
FILE tests/typeAppsForbidden.hs
52+
{-# LANGUAGE TypeApplications #-}
53+
module TypeAppsForbidden where
54+
55+
a x = id @Int x
56+
b x = id x
57+
OUTPUT
58+
tests/typeAppsForbidden.hs:4:7-8: Warning: Avoid visible type application
59+
Found:
60+
id
61+
Note: may break the code
62+
63+
1 hint
64+
65+
---------------------------------------------------------------------

0 commit comments

Comments
 (0)