summaryrefslogtreecommitdiff
path: root/tests/Language/PureScript/Ide/ImportsSpec.hs
blob: 908531b995c0cf47e3d66c52e00887183889f233 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Language.PureScript.Ide.ImportsSpec where

import           Protolude
import           Data.Maybe                      (fromJust)

import qualified Language.PureScript             as P
import           Language.PureScript.Ide.Command as Command
import           Language.PureScript.Ide.Error
import           Language.PureScript.Ide.Imports
import qualified Language.PureScript.Ide.Test as Test
import           Language.PureScript.Ide.Types
import           System.FilePath
import           Test.Hspec

noImportsFile :: [Text]
noImportsFile =
  [ "module Main where"
  , ""
  , "myFunc x y = x + y"
  ]

simpleFile :: [Text]
simpleFile =
  [ "module Main where"
  , "import Prelude"
  , ""
  , "myFunc x y = x + y"
  ]

syntaxErrorFile :: [Text]
syntaxErrorFile =
  [ "module Main where"
  , "import Prelude"
  , ""
  , "myFunc ="
  ]

splitSimpleFile :: (P.ModuleName, [Text], [Import], [Text])
splitSimpleFile = fromRight (sliceImportSection simpleFile)
  where
    fromRight = fromJust . rightToMaybe

withImports :: [Text] -> [Text]
withImports is =
  take 2 simpleFile ++ [""] ++ is ++ drop 2 simpleFile

testParseImport :: Text -> Import
testParseImport = fromJust . parseImport

preludeImport, arrayImport, listImport, consoleImport, maybeImport :: Import
preludeImport = testParseImport "import Prelude"
arrayImport = testParseImport "import Data.Array (head, cons)"
listImport = testParseImport "import Data.List as List"
consoleImport = testParseImport "import Control.Monad.Eff.Console (log) as Console"
maybeImport = testParseImport "import Data.Maybe (Maybe(Just))"

spec :: Spec
spec = do
  describe "determining the importsection" $ do
    let moduleSkeleton imports =
          Right (P.moduleNameFromString "Main", take 1 simpleFile, imports, drop 2 simpleFile)
    it "slices a file without imports and adds a newline after the module declaration" $
      shouldBe (sliceImportSection noImportsFile)
          (Right (P.moduleNameFromString "Main", take 1 noImportsFile ++ [""], [], drop 1 noImportsFile))

    it "handles a file with syntax errors just fine" $
      shouldBe (sliceImportSection syntaxErrorFile)
      (Right (P.moduleNameFromString "Main", take 1 syntaxErrorFile, [preludeImport], drop 2 syntaxErrorFile))

    it "finds a simple import" $
      shouldBe (sliceImportSection simpleFile) (moduleSkeleton [preludeImport])

    it "allows multiline import statements" $
      shouldBe
        (sliceImportSection (withImports [ "import Data.Array (head,"
                                         , "                   cons)"
                                         ]))
        (moduleSkeleton [preludeImport, arrayImport])
    it "allows multiline import statements with hanging parens" $
      shouldBe
        (sliceImportSection (withImports [ "import Data.Array ("
                                         , "  head,"
                                         , "  cons"
                                         , ")"
                                         ]))
        (moduleSkeleton [preludeImport, arrayImport])
  describe "pretty printing imports" $ do
    it "pretty prints a simple import" $
      shouldBe (prettyPrintImport' preludeImport) "import Prelude"
    it "pretty prints an explicit import" $
      shouldBe (prettyPrintImport' arrayImport) "import Data.Array (head, cons)"
    it "pretty prints a qualified import" $
      shouldBe (prettyPrintImport' listImport) "import Data.List as List"
    it "pretty prints a qualified explicit import" $
      shouldBe (prettyPrintImport' consoleImport) "import Control.Monad.Eff.Console (log) as Console"
    it "pretty prints an import with a datatype (and PositionedRef's for the dtors)" $
      shouldBe (prettyPrintImport' maybeImport) "import Data.Maybe (Maybe(Just))"

  describe "import commands" $ do
    let simpleFileImports = let (_, _, i, _) = splitSimpleFile in i
        addValueImport i mn is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideValue i Nothing)) mn is)
        addOpImport op mn is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideValueOp op (P.Qualified Nothing (Left "")) 2 Nothing Nothing)) mn is)
        addDtorImport i t mn is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideDtor i t Nothing)) mn is)
        addTypeImport i mn is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideType i Nothing)) mn is)
        addKindImport i mn is =
          prettyPrintImportSection (addExplicitImport' (_idaDeclaration (Test.ideKind i)) mn is)
    it "adds an implicit unqualified import to a file without any imports" $
      shouldBe
        (addImplicitImport' [] (P.moduleNameFromString "Data.Map"))
        ["import Data.Map"]
    it "adds an implicit unqualified import" $
      shouldBe
        (addImplicitImport' simpleFileImports (P.moduleNameFromString "Data.Map"))
        [ "import Data.Map"
        , "import Prelude"
        ]
    it "adds a qualified import" $
      shouldBe
        (addQualifiedImport' simpleFileImports (Test.mn "Data.Map") (Test.mn "Map"))
        [ "import Prelude"
        , ""
        , "import Data.Map as Map"
        ]
    it "adds an explicit unqualified import to a file without any imports" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") [])
        ["import Data.Array (head)"]
    it "adds an explicit unqualified import" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") simpleFileImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (head)"
        ]
    it "doesn't add an import if the containing module is imported implicitly" $
      shouldBe
      (addValueImport "const" (P.moduleNameFromString "Prelude") simpleFileImports)
      ["import Prelude"]
    let Right (_, _, explicitImports, _) = sliceImportSection (withImports ["import Data.Array (tail)"])
    it "adds an identifier to an explicit import list" $
      shouldBe
        (addValueImport "head" (P.moduleNameFromString "Data.Array") explicitImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (head, tail)"
        ]
    it "adds a kind to an explicit import list" $
      shouldBe
        (addKindImport "Effect" (P.moduleNameFromString "Control.Monad.Eff") simpleFileImports)
        [ "import Prelude"
        , ""
        , "import Control.Monad.Eff (kind Effect)"
        ]
    it "adds an operator to an explicit import list" $
      shouldBe
        (addOpImport "<~>" (P.moduleNameFromString "Data.Array") explicitImports)
        [ "import Prelude"
        , ""
        , "import Data.Array (tail, (<~>))"
        ]
    it "adds a type with constructors without automatically adding an open import of said constructors " $
        shouldBe
          (addTypeImport "Maybe" (P.moduleNameFromString "Data.Maybe") simpleFileImports)
          [ "import Prelude"
          , ""
          , "import Data.Maybe (Maybe)"
          ]
    it "adds the type for a given DataConstructor" $
        shouldBe
          (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") simpleFileImports)
          [ "import Prelude"
          , ""
          , "import Data.Maybe (Maybe(..))"
          ]
    it "adds a dataconstructor to an existing type import" $ do
      let Right (_, _, typeImports, _) = sliceImportSection (withImports ["import Data.Maybe (Maybe)"])
      shouldBe
        (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") typeImports)
        [ "import Prelude"
        , ""
        , "import Data.Maybe (Maybe(..))"
        ]
    it "doesn't add a dataconstructor to an existing type import with open dtors" $ do
      let Right (_, _, typeImports, _) = sliceImportSection (withImports ["import Data.Maybe (Maybe(..))"])
      shouldBe
        (addDtorImport "Just" "Maybe" (P.moduleNameFromString "Data.Maybe") typeImports)
        [ "import Prelude"
        , ""
        , "import Data.Maybe (Maybe(..))"
        ]
    it "doesn't add an identifier to an explicit import list if it's already imported" $
      shouldBe
      (addValueImport "tail" (P.moduleNameFromString "Data.Array") explicitImports)
      [ "import Prelude"
      , ""
      , "import Data.Array (tail)"
      ]

  describe "explicit import sorting" $ do
    -- given some basic import skeleton
    let Right (_, _, baseImports, _) = sliceImportSection $ withImports ["import Control.Monad (ap)"]
        moduleName = (P.moduleNameFromString "Control.Monad")
        addImport imports import' = addExplicitImport' import' moduleName imports
        valueImport ident = _idaDeclaration (Test.ideValue ident Nothing)
        typeImport name = _idaDeclaration (Test.ideType name Nothing)
        classImport name = _idaDeclaration (Test.ideTypeClass name P.kindType [])
        dtorImport name typeName = _idaDeclaration (Test.ideDtor name typeName Nothing)
        -- expect any list of provided identifiers, when imported, to come out as specified
        expectSorted imports expected = shouldBe
          (ordNub $ map
            (prettyPrintImportSection . foldl addImport baseImports)
            (permutations imports))
          [expected]
    it "sorts class" $
      expectSorted (map classImport ["Applicative", "Bind"])
        ["import Prelude", "", "import Control.Monad (class Applicative, class Bind, ap)"]
    it "sorts value" $
      expectSorted (map valueImport ["unless", "where"])
        ["import Prelude", "", "import Control.Monad (ap, unless, where)"]
    it "sorts type, value" $
      expectSorted
        ((map valueImport ["unless", "where"]) ++ (map typeImport ["Foo", "Bar"]))
        ["import Prelude", "", "import Control.Monad (Bar, Foo, ap, unless, where)"]
    it "sorts class, type, value" $
      expectSorted
        ((map valueImport ["unless", "where"]) ++ (map typeImport ["Foo", "Bar"]) ++ (map classImport ["Applicative", "Bind"]))
        ["import Prelude", "", "import Control.Monad (class Applicative, class Bind, Bar, Foo, ap, unless, where)"]
    it "sorts types with constructors, using open imports for the constructors" $
      expectSorted
        -- the imported names don't actually have to exist!
        (map (uncurry dtorImport) [("Just", "Maybe"), ("Nothing", "Maybe"), ("SomeOtherConstructor", "SomeDataType")])
        ["import Prelude", "", "import Control.Monad (Maybe(..), SomeDataType(..), ap)"]
  describe "importing from a loaded IdeState" importFromIdeState

implImport :: Text -> Command
implImport mn =
  Command.Import ("src" </> "ImportsSpec.purs") Nothing [] (Command.AddImplicitImport (Test.mn mn))

addExplicitImport :: Text -> Command
addExplicitImport i =
  Command.Import ("src" </> "ImportsSpec.purs") Nothing [] (Command.AddImportForIdentifier i)

importShouldBe :: [Text] -> [Text] -> Expectation
importShouldBe res importSection =
  res `shouldBe` [ "module ImportsSpec where" , ""] ++ importSection ++ [ "" , "myId x = x"]

runIdeLoaded :: Command -> IO (Either IdeError Success)
runIdeLoaded c = do
  ([_, result], _) <- Test.inProject $ Test.runIde [Command.LoadSync [] , c]
  pure result

importFromIdeState :: Spec
importFromIdeState = do
  it "adds an implicit import" $ do
    Right (MultilineTextResult result) <-
      runIdeLoaded (implImport "ImportsSpec1")
    result `importShouldBe` [ "import ImportsSpec1" ]
  it "adds an explicit unqualified import" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "exportedFunction")
    result `importShouldBe` [ "import ImportsSpec1 (exportedFunction)" ]
  it "adds an explicit unqualified import (type)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyType")
    result `importShouldBe` [ "import ImportsSpec1 (MyType)" ]
  it "adds an explicit unqualified import (parameterized type)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyParamType")
    result `importShouldBe` [ "import ImportsSpec1 (MyParamType)" ]
  it "adds an explicit unqualified import (typeclass)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "ATypeClass")
    result `importShouldBe` [ "import ImportsSpec1 (class ATypeClass)" ]
  it "adds an explicit unqualified import (dataconstructor)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyJust")
    result `importShouldBe` [ "import ImportsSpec1 (MyMaybe(..))" ]
  it "adds an explicit unqualified import (newtype)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "MyNewtype")
    result `importShouldBe` [ "import ImportsSpec1 (MyNewtype(..))" ]
  it "adds an explicit unqualified import (typeclass member function)" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "typeClassFun")
    result `importShouldBe` [ "import ImportsSpec1 (typeClassFun)" ]
  it "doesn't add a newtypes constructor if only the type is exported" $ do
    Right (MultilineTextResult result) <-
      runIdeLoaded (addExplicitImport "OnlyTypeExported")
    result `importShouldBe` [ "import ImportsSpec1 (OnlyTypeExported)" ]
  it "doesn't add an import if the identifier is defined in the module itself" $ do
    Right (MultilineTextResult result) <- runIdeLoaded (addExplicitImport "myId")
    result `importShouldBe` []
  it "responds with an error if it's undecidable whether we want a type or constructor" $ do
    result <- runIdeLoaded (addExplicitImport "SpecialCase")
    result `shouldSatisfy` isLeft
  it "responds with an error if the identifier cannot be found and doesn't \
     \write to the output file" $ do
    result <- runIdeLoaded (addExplicitImport "doesnExist")
    result `shouldSatisfy` isLeft