packages feed

hnix-0.17.0: tests/ParserTests.hs

{-# language QuasiQuotes #-}
{-# language TemplateHaskell #-}
{-# language RankNTypes #-}
{-# language ExtendedDefaultRules #-}

{-# options_ghc -fno-warn-name-shadowing #-}
{-# options_ghc -Wno-missing-signatures #-}
{-# options_ghc -Wno-type-defaults #-}


module ParserTests (tests) where

import Nix.Prelude hiding (($<))
import Data.Fix
import NeatInterpolation (text)
import Nix.Atoms
import Nix.Expr
import Nix.Parser
import Nix.Pretty
import Prettyprinter
import Prettyprinter.Render.Text
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.TH
import Prettyprinter.Render.String (renderString)
import Prettyprinter.Util (reflow)

default (NixLang)

-- * Tests


-- ** Literals

case_constant_int =
  checks
    ( mkInt 234
    , "234"
    )

case_constant_bool =
  checks
    ( mkBool True
    , "true"
    )
    ( mkBool False
    , "false"
    )

case_constant_bool_respects_attributes =
  invariantVals
    "true-foo"
    "false-bar"

case_constant_path_invariants =
  knownAs (staysInvariantUnder (mkRelPath . toString))
    "./."
    "./+-_/cdef/09ad+-"
    "/abc"
    "../abc"
    "~/a"
    "~/a/b"
    "a/b"
    "4/2"
    "rec+def/cdef"

case_constant_path =
  checks
    ( var "a'b" @@ mkRelPath "/c"
    , "a'b/c"
    )
    ( mkRelPath "a/b" $// mkRelPath "c/def" $// mkEnvPath "g" $<  mkRelPath "def/d"
    , "a/b//c/def//<g> < def/d"
    )
    ( mkEnvPath "abc"
    , "<abc>"
    )
    ( mkEnvPath "../cdef"
    , "<../cdef>"
    )
    ( var "a" $// var "b"
    , "a//b"
    )

case_constant_path_syntax_mistakes =
  mistakes
    "."
    ".."
    "/"
    "a/"
    "a/def/"
    "~"
    "~/"

case_constant_uri =
  knownAs (staysInvariantUnder mkStr)
    "a:a"
    "http://foo.bar"
    "a+de+.adA+-:%%%ads%5asdk&/"
    "rec+def:c"
    "f.foo:bar"


case_constant_uri_syntax_mistakes =
  mistakes
    "http://foo${\"bar\"}"
    ":bcdef"
    "a%20:asda"
    ".:adasd"
    "+:acdcd"


-- *** Special chars in vals

case_identifier_special_chars =
  invariantVals
    "_a"
    "a_b"
    "a'b"
    "a''b"
    "a-b"
    "a--b"
    "a12a"

case_identifier_special_chars_syntax_mistakes =
  mistakes
    ".a"
    "'a"

-- ** Sets

-- *** Non-recursive sets

case_simple_set =
  checks
    ( mkNonRecSet
        [ "a" $= mkInt 23
        , "b" $= mkInt  4
        ]
    , "{ a = 23; b = 4; }"
    )

case_simple_set_syntax_mistakes =
  mistakes
    "{ a = 23 }"

case_set_complex_keynames =
  checks
    ( mkNonRecSet $
        one (NamedVar (one (DynamicKey (Plain (DoubleQuoted mempty)))) mkNull nullPos)
    , "{ \"\" = null; }"
    )
    ( mkNonRecSet
        [ NamedVar (StaticKey "a" :| one (StaticKey "b")) (mkInt 3) nullPos
        , NamedVar (StaticKey "a" :| one (StaticKey "c")) (mkInt 4) nullPos
        ]
    , "{ a.b = 3; a.c = 4; }"
    )
    ( mkNonRecSet $
        one (NamedVar (one (DynamicKey (Antiquoted letExpr))) (mkInt 4) nullPos)
    , "{ ${let a = \"b\"; in a} = 4; }"
    )
    ( mkNonRecSet $
        one (NamedVar (DynamicKey (Plain str) :| one (StaticKey "e")) (mkInt 4) nullPos)
    , "{ \"a${let a = \"b\"; in a}c\".e = 4; }"
    )
 where
  letExpr = mkLets (one ("a" $= mkStr "b")) (var "a")
  str = DoubleQuoted [Plain "a", Antiquoted letExpr, Plain "c"]


-- *** Recursivity in sets

case_set_rec =
  checks
    ( mkRecSet
        [ "a" $= mkInt 3
        , "b" $= var "a"
        ]
    , "rec { a = 3; b = a; }"
    )


-- *** Inheritance

case_set_inherit =
  checks
    ( mkNonRecSet
        [ "e" $= mkInt 3
        , inherit ["a", "b"]
        ]
    , "{ e = 3; inherit a b; }"
    )
    ( mkNonRecSet $ one $ inherit mempty
    , "{ inherit; }"
    )

case_set_scoped_inherit =
  checks
    ( mkNonRecSet $
        (\ x -> [x, "e" $= mkInt 4, x]) $
          inheritFrom (var "a") ["b", "c"]
    , "{ inherit (a) b c; e = 4; inherit(a)b c; }"
    )

case_set_inherit_direct =
  checks
    ( mkNonRecSet $ one (inheritFrom (mkNonRecSet $ one ("a" $= mkInt 3)) mempty)
    , "{ inherit ({a = 3;}); }"
    )

case_inherit_selector_syntax_mistakes =
  mistakes
    "{ inherit a.x; }"
    -- A rare quirk of Nix that is proper to fix then to support (see git commit history)
    -- (old parser test result was):
    -- mkNonRecSet [inherit [DynamicKey (Plain (DoubleQuoted [Plain "a"]))]],
    "{ inherit \"a\"; }"


-- ** Lists

case_int_list =
  checks
    ( mkList $ mkInt <$> [1,2,3]
    , "[1 2 3]"
    )

case_int_null_list =
  checks
    ( mkList (mkConst <$> [NInt 1, NInt 2, NInt 3, NNull, NInt 4])
    , "[1 2 3 null 4]"
    )

case_mixed_list =
  checks
    ( mkList
        [ mkNonRecSet (one $ "a" $= mkInt 3) @. "a"
        , mkIf (mkBool True) mkNull (mkBool False)
        , mkNull
        , mkBool False
        , mkInt 4
        , emptyList
        , (@.<|>) (var "c") "d" mkNull
        ]
    , "[{a = 3;}.a (if true then null else false) null false 4 [] c.d or null]"
    )

case_mixed_list_syntax_mistakes =
  mistakes
    "[if true then null else null]"
    "[a ? b]"
    "[a : a]"
    "[${\"test\")]"


-- ** Lambdas

case_simple_lambda =
  checks
    ( mkFunction (Param "a") (var "a")
    , "a: a"
    )

case_lambda_or_uri =
  checks
    ( mkFunction (Param "a") $ var "b"
    , "a :b"
    )
    ( var "a" @@ mkStr "c:def"
    , "a c:def"
    )
    ( mkStr "c:def:" @@ var "c"
    , "c:def: c"
    )
    ( mkFunction (Param "a") emptySet
    , "a:{}"
    )
    ( mkFunction (Param "a") $ mkList $ one $ var "a"
    , "a:[a]"
    )

case_lambda_or_uri_syntax_mistakes =
  mistakes
    "def:"

case_lambda_pattern =
  checks
    ( mkFunction (mkParamSet args) $ var "b"
    , "{b, c ? 1}: b"
    -- Fix (NAbs (ParamSet [("b",Nothing),("c",Just (Fix (NConstant (NInt 1))))] False Nothing) (Fix (NSym "b")))
    )
    ( mkFunction (mkParamSet args2) $ var "b"
    , "{ b ? x: x  }: b"
    )
    ( mkFunction (mkNamedParamSet "a" args) $ var "b"
    , "a@{b,c ? 1}: b"
    )
    ( mkFunction (mkNamedParamSet "a" args) $ var "c"
    , "{b,c?1}@a: c"
    )
    ( mkFunction (mkNamedVariadicParamSet "a" vargs) $ var "c"
    , "{b,c?1,...}@a: c"
    )
    ( mkFunction (mkVariadicParamSet mempty) $ mkInt 1
    , "{...}: 1"
    )
 where
  args  = [("b", Nothing), ("c", pure $ mkInt 1)]
  vargs = [("b", Nothing), ("c", pure $ mkInt 1)]
  args2 = one ("b", pure lam)
  lam = mkFunction (Param "x") $ var "x"

case_lambda_pattern_syntax_mistakes =
  mistakes
    "a@b: a"
    "{a}@{b}: a"

case_lambda_app_int =
  checks
    ( mkFunction (Param "a") (var "a") @@ mkInt 3
    , "(a: a) 3"
    )


-- ** Let

case_simple_let =
  checks
    ( mkLets (one $ "a" $= mkInt 4) $ var "a"
    , "let a = 4; in a"
    )

case_simple_let_syntax_mistakes =
  mistakes
    "let a = 4 in a"

case_let_body =
  checks
    ( mkRecSet (one $ "body" $= mkInt 1) @. "body"
    , "let { body = 1; }"
    )

case_nested_let =
  checks
    ( mkLets (one $ "a" $= mkInt 4) $
        mkLets (one $ "b" $= mkInt 5) $ var "a"
    , "let a = 4; in let b = 5; in a"
    )

case_nested_let_syntax_mistakes =
  mistakes
    "let a = 4; let b = 3; in b"

case_let_scoped_inherit =
  checks
    ( mkLets
        [ "a" $= mkNull
        , inheritFrom (var "b") $ one "c"
        ]
        $ var "c"
    , "let a = null; inherit (b) c; in c"
    )

case_let_scoped_inherit_syntax_mistakes =
  mistakes
    "let inherit (b) c in c"


-- ** If

case_if =
  checks
    ( mkIf (mkBool True) (mkBool True) (mkBool False)
    , "if true then true else false"
    )

case_if_syntax_mistakes =
  mistakes
    "if true then false"
    "else"
    "if true then false else"
    "if true then false else false else"
    "1 + 2 then"

-- ** If follow by with

case_if_follow_by_with = checks 
  (
    mkLets (one $ "x" $= mkNonRecSet ["a" $= mkBool True, "b" $= mkInt 2 ]) 
      $ mkIf (mkWith (mkSym "x") (mkSym "a")) (mkInt 1) (mkInt 2)
    , "let x = { a = true; b = 2; }; in if with x; a then 1 else 2"
  )

-- ** Literal expressions in vals

case_identifier_keyword_prefix_invariants =
  invariantVals
    "true-name"
    "trueName"
    "null-name"
    "nullName"

case_identifier_keyword_prefix =
  checks
    ( mkList $ one $ var "null-name"
    , "[ null-name ]"
    )


-- ** Strings

invariantString str =
  checks
    ( mkStr str
    , "\"" <> str <> "\""
    )

case_simple_string =
  knownAs invariantString
    "abcdef"
    "a"
    "A"
    "   a a  "
    ""

case_string_dollar =
  knownAs invariantString
    "a$b"
    "a$$b"
    "$cdef"
    "gh$i"

case_string_escape =
  checks
    ( mkStr "$\n\t\r\\"
    , "\"\\$\\n\\t\\r\\\\\""
    )
    ( mkStr " \" ' "
    , "\" \\\" \\' \""
    )

case_string_antiquote =
  checks
    ( Fix $ NStr $ DoubleQuoted
        [ Plain "abc"
        , Antiquoted $ mkIf (mkBool True) (mkStr "def") (mkStr "abc")
        , Plain " g"
        ]
    , "\"abc${  if true then \"def\" else \"abc\"  } g\""
    )
    ( mkStr "${a}"
    , "\"\\${a}\""
    )

case_string_antiquote_syntax_mistakes =
  mistakes
    "\"a"
    "${true}"
    "\"${true\""


-- *** Indented string

case_indented_string =
  checks
    ( mkIndentedStr 0 "a"
    , "''a''"
    )
    ( mkIndentedStr 2 "foo\nbar"
    , "''\n  foo\n  bar''"
    )
    ( mkIndentedStr 0 mempty
    , "''        ''"
    )
    ( mkIndentedStr 0 "''"
    , "'''''''"
    )
    ( Fix $ NStr $ Indented 3
        [ Antiquoted mkNull
        , Plain "\na"
        , Antiquoted mkNull
        ]
    , "''   ${null}\n   a${null}''"
    )

case_indented_string_syntax_mistakes =
  mistakes
    "'''''"
    "''   '"

case_indented_string_escape =
  checks
    ( mkIndentedStr 1 "\n \t \\ ${ \\ \\n ' '' "
    , "'' ''\\n ''\\t ''\\\\ ''${ \\ \\n ' ''' ''"
    )

-- ** Selection

case_select =
  checks
    ( Fix $ NSelect Nothing (var "a") (StaticKey "e" :| [StaticKey "di", StaticKey "f"])
    , "a .  e .di. f"
    )
    ( Fix $ NSelect (pure mkNull) (var "a")
        (StaticKey "e" :| one (StaticKey "d"))
    , "a.e . d    or null"
    )
    ( Fix $ NSelect (pure mkNull) emptySet
        (one $ DynamicKey (Plain $ DoubleQuoted mempty))
    , "{}.\"\"or null"
    )
    ( Fix $ NBinary NConcat
        ((@.<|>)
          (mkNonRecSet $
            one $
              NamedVar
                (mkSelector "a")
                (mkList $ one $ mkInt 1)
                nullPos
          )
          "a"
          (mkList $ one $ mkInt 2)
        )
        (mkList $ one $ mkInt 3)
    , "{ a = [1]; }.a or [2] ++ [3]"
    )

case_select_path =
  checks
    ( var "f" @@ mkRelPath "./."
    , "f ./."
    )
    ( var "f" @. "b" @@ mkRelPath "../a"
    , "f.b ../a"
    )
    ( emptySet @@ mkRelPath "./def"
    , "{}./def"
    )
    ( Fix (NSelect Nothing emptySet $ one $ DynamicKey $ Plain $ DoubleQuoted mempty) @@ mkRelPath "./def"
    , "{}.\"\"./def"
    )

case_select_keyword =
  checks
    ( mkNonRecSet $ one $ "false" $= mkStr "foo"
    , "{ false = \"foo\"; }"
    )

case_select_or_precedence =
    assertParsePrint
      [text|
        let
          matchDef = def:   matcher:
                              v:   let
                                    case = builtins.head (builtins.attrNames v);
                                  in (matcher.case or def case) (v.case);
        in null
      |]
      [text|
         let
          matchDef = def:
            matcher:
              v:
                let
                  case = builtins.head (builtins.attrNames v);
                in (matcher.case or def) case (v.case);
        in null
      |]

case_select_or_precedence2 =
    assertParsePrint
      [text|
        let
          matchDef = def:   matcher:
                              v:   let
                                    case = builtins.head (builtins.attrNames v);
                                  in (matcher.case or null.foo) (v.case);
        in null
      |]
      [text|
        let
          matchDef = def:
            matcher:
              v:
                let
                  case = builtins.head (builtins.attrNames v);
                in (matcher.case or null).foo (v.case);
        in null
      |]

-- ** Function application

case_fun_app =
  checks
    ( var "f" @@ var "a" @@ var "b"
    , "f a b"
    )
    ( var "f" @@ (@.<|>) (var "a") "x" mkNull
    , "f a.x or null"
    )

case_fun_app_syntax_mistakes =
  mistakes
   "f if true then null else null"


-- ** Operators

case_operator_fun_app =
  checks
    ( var "a" $++ var "b"
    , "a ++ b"
    )
    ( var "a" $++ var "f" @@ var "b"
    , "a ++ f b"
    )

case_operators =
  checks
    ( mkInt 1 $+ mkInt 2 $- mkInt 3
    , "1 + 2 - 3"
    )
    ( mkInt 1 $+ mkIf (mkBool True) (mkInt 2) (mkInt 3)
    , "1 + (if true then 2 else 3)"
    )
    ( mkNonRecSet (one $ "a" $= mkInt 3) $// mkRecSet (one $ "b" $= mkInt 4)
    , "{ a = 3; } // rec { b = 4; }"
    )
    ( mkNeg $ mkNeg $ var "a"
    , "--a"
    )
    ( var "a" $- var "b" $- var "c"
    , "a - b - c"
    )
    ( var "foo" $< var "bar"
    , "foo<bar"
    )

case_operators_syntax_mistakes =
  mistakes
    "+ 3"
    "foo +"
    "1 + if true then 1 else 2"


-- ** Comments

case_comments =
  do
    Right expected <- parseNixFile "data/let.nix"
    assertParseFile "let-comments-multiline.nix" expected
    assertParseFile "let-comments.nix" expected


-- ** Location

case_simpleLoc =
  let
    mkSPos = on (NSourcePos "<string>") (coerce . mkPos)
    mkSpan = on SrcSpan (uncurry mkSPos)
  in
    assertParseTextLoc [text|let
    foo = bar
         baz "qux";
    in foo
    |]
    (NLetAnn
      (mkSpan (1, 1) (4, 7))
      (one $
        NamedVar
          (one $ StaticKey "foo")
          (NAppAnn
            (mkSpan (2, 7) (3, 15))
            (NAppAnn
              (mkSpan (2, 7) (3, 9))
              (NSymAnn (mkSpan (2, 7) (2, 10)) "bar")
              (NSymAnn (mkSpan (3, 6) (3, 9 )) "baz")
            )
            (NStrAnn (mkSpan (3, 10) (3, 15)) $ DoubleQuoted $ one $ Plain "qux")
          )
          (mkSPos 2 1)
      )
      (NSymAnn (mkSpan (4, 4) (4, 7)) "foo")
    )


tests :: TestTree
tests = $testGroupGenerator

---------------------------------------------------------------------------------

-- * Helpers

var = mkSym

invariantVal = staysInvariantUnder var

staysInvariantUnder :: (NixLang -> ExpectedHask) -> NixLang -> Assertion
staysInvariantUnder f v =
  (<=>) (f v) v

type NixLang = Text
type ExpectedHask = NExpr

(<=>) :: ExpectedHask -> NixLang -> Assertion
(<=>) = assertParseText

throwParseError :: forall ann . Text -> Text -> Doc ann -> Assertion
throwParseError entity expr err =
  assertFailure $
    renderString $
      layoutSmart Prettyprinter.defaultLayoutOptions $
        nest 2 $
          vsep
            [ mempty
            , "Unexpected fail parsing " <> reflow entity <> ":"
            , nest 2 $ vsep
              [ "Expression:"
              , reflow expr
              , "Error: " <> nest 2 err
              ]
            ]

assertParseText :: ExpectedHask -> NixLang -> Assertion
assertParseText expected str =
  either
    (throwParseError "expression" str)
    (assertEqual
      ("When parsing " <> toString str)
      (stripPositionInfo expected)
      . stripPositionInfo
    )
    (parseNixText str)

assertParseTextLoc :: NixLang -> NExprLoc -> Assertion
assertParseTextLoc str expected =
  either
    (throwParseError "expression" str)
    (assertEqual
      ("When parsing " <> toString str)
      expected
    )
    (parseNixTextLoc str)

assertParseFile :: Path -> NExpr -> Assertion
assertParseFile file expected =
  do
    res <- parseNixFile $ "data/" <> file
    either
      (throwParseError "data file" $ coerce fromString file)
      (assertEqual
        ("Parsing data file " <> coerce file)
        (stripPositionInfo expected)
        . stripPositionInfo
      )
      res

assertParseFail :: NixLang -> Assertion
assertParseFail str =
  either
    (const stub)
    (\ r ->
      assertFailure $ toString $ "\nUnexpected success parsing string ''" <> str <> "'':\n''Parsed value: ''" <> show r <> "''."
    )
    (parseNixText str)

-- assertRoundTrip :: Text -> Assertion
-- assertRoundTrip src = assertParsePrint src src

assertParsePrint :: Text -> Text -> Assertion
assertParsePrint src expect =
  let
    Right expr = parseNixTextLoc src
    result =
      renderStrict
      . layoutPretty defaultLayoutOptions
      . prettyNix
      . stripAnnotation $
        expr
  in
  assertEqual mempty expect result


-----

-- | This class constructs functions that accept variacic number of argumets.
-- Every argument is an assertion.
-- So now the new assertions can be added just by adding it to a block of according assertions.
class VariadicAssertions t where
  checkListPairs' :: ((ExpectedHask, NixLang) -> Assertion) -> [(ExpectedHask, NixLang)] -> t

instance VariadicAssertions (IO a) where
  checkListPairs' f acc =
    do
      traverse_ f acc
      pure $ error "never would be reached, cuz `I'm lazy`."

instance (VariadicAssertions a) => VariadicAssertions ((ExpectedHask, NixLang) -> a) where
  checkListPairs' f acc x = checkListPairs' f (acc <> one x)

checks :: (VariadicAssertions a) => a
checks = checkListPairs' (uncurry assertParseText) mempty


class VariadicArgs t where
  checkList' :: (NixLang -> Assertion) -> [NixLang] -> t

instance VariadicArgs (IO a) where
  checkList' f acc =
    do
      traverse_ f acc
      pure $ error "never would be reached, cuz `I'm lazy`."

instance (VariadicArgs a) => VariadicArgs (NixLang -> a) where
  checkList' f acc x = checkList' f (acc <> one x)

knownAs :: (VariadicArgs a) => (NixLang -> Assertion) -> a
knownAs f = checkList' f mempty

mistakes :: (VariadicArgs a) => a
mistakes = knownAs assertParseFail

invariantVals :: (VariadicArgs a) => a
invariantVals = knownAs invariantVal