Skip to content

Commit cac90ed

Browse files
committed
Fix bugs in UndesirableFunctions, add more tests
1 parent 26e7e70 commit cac90ed

File tree

5 files changed

+88
-23
lines changed

5 files changed

+88
-23
lines changed

dist/npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "whine",
3-
"version": "0.0.26",
3+
"version": "0.0.27",
44
"description": "PureScript linter, extensible, with configurable rules, and one-off escape hatches",
55
"keywords": ["purescript", "lint"],
66
"author": "Fyodor Soikin <name.fa@gmail.com>",

dist/vscode-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "collegevine",
44
"displayName": "Whine at PureScript",
55
"description": "PureScript linter, extensible, with configurable rules, and one-off escape hatches",
6-
"version": "0.0.26",
6+
"version": "0.0.27",
77
"repository": "https://github.com/collegevine/purescript-whine",
88
"engines": {
99
"vscode": "^1.95.0"

spago.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ package:
5151
dependencies:
5252
- spec-node
5353
publish:
54-
version: 0.0.26
54+
version: 0.0.27
5555
license: MIT
5656
location:
5757
githubOwner: collegevine

src/Whine/Core/UndesirableFunctions.purs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import Data.Map as Map
2424
import Data.String as String
2525
import PureScript.CST.Range (rangeOf)
2626
import PureScript.CST.Types (Expr(..), Ident(..), Import(..), ImportDecl(..), Module(..), ModuleHeader(..), ModuleName, Name(..), Operator(..), QualifiedName(..), Separated(..), Wrapped(..))
27+
import Whine.Log (logDebug)
2728
import Whine.Types (Handle(..), Rule, currentModule, emptyRule, reportViolation)
2829

2930
-- Config for this rule is a two-level map: first level is names of functions
@@ -58,10 +59,15 @@ rule badFunctions = emptyRule { onExpr = onExpr }
5859
e@(ExprIdent (QualifiedName { name: Ident function, module: mod }))
5960
| Just mods <- Map.lookup { function } badFunctions -> -- This function is on the list of undesirables.
6061
currentModule \m -> do
62+
logDebug $ "Checking " <> function <> " from " <> show (unwrap <$> mod)
63+
logDebug $ "Modules: " <> show ((lmap $ map unwrap) <$> Map.toUnfoldable mods :: Array _)
6164
let report message = reportViolation { source: Just $ rangeOf e, message }
62-
importedFrom = explicitOrQualifiedImport m mod function <|> loneOpenImport m
65+
importedFrom = qualifiedImport m mod <|> explicitImport m function <|> loneOpenImport m
66+
logDebug $ "Imported from " <> show (unwrap <$> importedFrom)
6367
case importedFrom of
6468
Just imprt -> do -- Found whence this function is imported.
69+
logDebug $ "Explicit message: " <> show (Map.lookup (Just imprt) mods)
70+
logDebug $ "Module-agnostic message: " <> show (Map.lookup Nothing mods)
6571
let message =
6672
Map.lookup (Just imprt) mods -- See if we have a message for this specific import.
6773
<|> Map.lookup Nothing mods -- If not, see if we have a module-agnostic message for this function.
@@ -74,16 +80,22 @@ rule badFunctions = emptyRule { onExpr = onExpr }
7480
_ -> do
7581
pure unit
7682

77-
-- Look through imports in the header of the module and find one that either
78-
-- (1) is qualified with the given qualifier `mod` or (2) lists the given
79-
-- identifier `ident` among imported values or operators.
80-
explicitOrQualifiedImport :: e. Module e -> Maybe ModuleName -> String -> Maybe ModuleName
81-
explicitOrQualifiedImport (Module { header: ModuleHeader { imports } }) mod ident =
83+
-- Look through imports in the header of the module and find one that is
84+
-- qualified with the given qualifier `mod`
85+
qualifiedImport :: e. Module e -> Maybe ModuleName -> Maybe ModuleName
86+
qualifiedImport _ Nothing = Nothing
87+
qualifiedImport (Module { header: ModuleHeader { imports } }) (Just mod) =
88+
imports # findMap \(ImportDecl i@{ module: Name { name } }) -> case i of
89+
{ qualified: Just (_ /\ Name { name: qualifier }) } | qualifier == mod ->
90+
Just name
91+
_ ->
92+
Nothing
93+
94+
-- Look through imports in the header of the module and find one that lists
95+
-- the given identifier `ident` among imported values or operators.
96+
explicitImport :: e. Module e -> String -> Maybe ModuleName
97+
explicitImport (Module { header: ModuleHeader { imports } }) ident =
8298
imports # findMap \(ImportDecl i@{ module: Name { name } }) -> case i of
83-
{ qualified: Just (_ /\ Name { name: qualifier }) }
84-
| Just qualifier == mod
85-
->
86-
Just name
8799
{ names: Just (_ /\ (Wrapped { value: Separated { head, tail } })) }
88100
| sameIdentifier head || any (sameIdentifier <<< snd) tail
89101
->
@@ -131,7 +143,7 @@ codec =
131143
}
132144

133145
foldMaps :: { functions :: Map String String } -> Args
134-
foldMaps mm = Map.unions do
146+
foldMaps mm = foldl (Map.unionWith Map.union) Map.empty do
135147
maybeQualifiedFunction /\ message <- (Map.toUnfoldable mm.functions :: Array _)
136148
let mod /\ function = fromQualified maybeQualifiedFunction
137149
pure $ Map.singleton { function } $ Map.singleton mod message

test/Core/UndesirableFunctions.purs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ module Test.Core.UndesirableFunctions where
22

33
import Test.Prelude
44

5+
import Codec.JSON.DecodeError as DecodeError
6+
import Control.Monad.Error.Class (throwError)
7+
import Data.Codec.JSON as CJ
58
import Data.Map as Map
9+
import Effect.Exception (error)
10+
import JSON as JSON
611
import PureScript.CST.Types (ModuleName(..))
712
import Test.Spec (Spec, describe, it)
813
import Test.Spec.Assertions (shouldEqual)
@@ -14,9 +19,9 @@ spec = describe "UndesirableFunctions" do
1419

1520
it "Reports bad functions" $
1621
hasViolations
17-
[ "4:5-4:8" /\ "Do not use fn1"
18-
, "5:4-5:7" /\ "Do not use fn2 from Bad.Module"
19-
, "6:4-6:7" /\ "Do not use fn3 from Bad.Module"
22+
[ "4:5-4:8 Do not use fn1"
23+
, "5:4-5:7 Do not use fn2 from Bad.Module"
24+
, "6:4-6:7 Do not use fn3 from Bad.Module"
2025
]
2126
"""
2227
import Some (fn1)
@@ -29,13 +34,13 @@ spec = describe "UndesirableFunctions" do
2934
"""
3035

3136
it "Falls back to unqualified message when there is no matching import" do
32-
hasViolations ["1:6-1:9" /\ "Do not use fn3"] """
37+
hasViolations ["1:6-1:9 Do not use fn3"] """
3338
import Some (fn3)
3439
x = y fn3
3540
"""
3641

3742
it "Assumes open-imported module if there is only one" do
38-
hasViolations ["2:6-2:9" /\ "Do not use fn3 from Bad.Module"] """
43+
hasViolations ["2:6-2:9 Do not use fn3 from Bad.Module"] """
3944
import Bad.Module
4045
import Another.Module (fn2)
4146
x = y fn3
@@ -49,7 +54,7 @@ spec = describe "UndesirableFunctions" do
4954
"""
5055

5156
it "Falls back to unqualified message when the import is open" do
52-
hasViolations ["1:6-1:9" /\ "Do not use fn3"] """
57+
hasViolations ["1:6-1:9 Do not use fn3"] """
5358
import Some
5459
x = y fn3
5560
"""
@@ -62,8 +67,8 @@ spec = describe "UndesirableFunctions" do
6267

6368
it "Reports when imported with a qualifier" do
6469
hasViolations
65-
[ "2:6-2:13" /\ "Do not use fn2 from Bad.Module"
66-
, "3:6-3:13" /\ "Do not use fn3 from Bad.Module"
70+
[ "2:6-2:13 Do not use fn2 from Bad.Module"
71+
, "3:6-3:13 Do not use fn3 from Bad.Module"
6772
]
6873
"""
6974
import Bad.Module as FN2
@@ -72,6 +77,18 @@ spec = describe "UndesirableFunctions" do
7277
y = z FN3.fn3
7378
"""
7479

80+
it "Picks correct message according to the import" do
81+
hasViolations
82+
[ "2:6-2:9 Do not use fn2 from Bad.Module"
83+
, "3:6-3:12 Do not use fn2 from Another.Module"
84+
]
85+
"""
86+
import Bad.Module (fn2)
87+
import Another.Module as AM
88+
x = y fn2
89+
y = z AM.fn2
90+
"""
91+
7592
it "Allows imports from other modules" do
7693
hasViolations [] """
7794
import Innocent.Module (fn2)
@@ -83,7 +100,43 @@ spec = describe "UndesirableFunctions" do
83100
x = y IM.fn2
84101
"""
85102

103+
describe "Config parsing" do
104+
it "parses correctly" do
105+
json <- unsafeRight $ JSON.parse """
106+
{
107+
"functions": {
108+
"fn1": "Do not use fn1",
109+
"Bad.fn1": "Do not use fn1 from Bad",
110+
"Bad.Module.fn1": "Do not use fn1 from Bad.Module",
111+
"Bad.fn2": "Do not use fn2 from Bad",
112+
"Bad.Module.fn2": "Do not use fn2 from Bad.Module"
113+
}
114+
}
115+
"""
116+
117+
parsed <- unsafeRight $ lmap DecodeError.print $
118+
CJ.decode UndesirableFunctions.codec json
119+
<#> Map.toUnfoldable
120+
<#> map \({ function } /\ m) ->
121+
function /\ (Map.toUnfoldable m <#> \(mn /\ msg) -> (unwrap <$> mn) /\ msg)
122+
123+
parsed `shouldEqual`
124+
[ "fn1" /\
125+
[ Nothing /\ "Do not use fn1"
126+
, Just "Bad" /\ "Do not use fn1 from Bad"
127+
, Just "Bad.Module" /\ "Do not use fn1 from Bad.Module"
128+
]
129+
, "fn2" /\
130+
[ Just "Bad" /\ "Do not use fn2 from Bad"
131+
, Just "Bad.Module" /\ "Do not use fn2 from Bad.Module"
132+
]
133+
]
86134
where
135+
unsafeRight :: a. Either String a -> _ a
136+
unsafeRight = case _ of
137+
Left e -> throwError $ error $ "Failed to parse config: " <> e
138+
Right a -> pure a
139+
87140
config = Map.fromFoldable
88141
[ { function: "fn1" } /\ Map.singleton
89142
Nothing "Do not use fn1"
@@ -102,5 +155,5 @@ spec = describe "UndesirableFunctions" do
102155
{ rule: UndesirableFunctions.rule config
103156
, module: mod
104157
}
105-
<#> map (\v -> fromMaybe "" (formatRange <$> v.source) /\ v.message)
158+
<#> map (\v -> fromMaybe "" (formatRange <$> v.source) <> " " <> v.message)
106159
>>= shouldEqual vs

0 commit comments

Comments
 (0)