{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternGuards     #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE ViewPatterns      #-}

-- | This module contains the implementation of the @dhall lint@ command

module Dhall.Lint
    ( -- * Lint
      lint
    , removeUnusedBindings
    , fixAssert
    , fixParentPath
    , addPreludeExtensions
    , removeLetInLet
    , useToMap
    ) where

import Control.Applicative ((<|>))

import Dhall.Syntax
    ( Binding (..)
    , Chunks (..)
    , Directory (..)
    , Expr (..)
    , File (..)
    , FilePrefix (..)
    , Import (..)
    , ImportHashed (..)
    , ImportType (..)
    , URL (..)
    , Var (..)
    , subExpressions
    )

import qualified Data.Foldable      as Foldable
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Text          as Text
import qualified Dhall.Core         as Core
import qualified Dhall.Map
import qualified Dhall.Optics
import qualified Lens.Family

{-| Automatically improve a Dhall expression

    Currently this:

    * removes unused @let@ bindings with 'removeUnusedBindings'.
    * fixes @let a = x ≡ y@ to be @let a = assert : x ≡ y@
    * consolidates nested @let@ bindings to use a multiple-@let@ binding with 'removeLetInLet'
    * fixes paths of the form @.\/..\/foo@ to @..\/foo@
-}
lint :: Eq s => Expr s Import -> Expr s Import
lint :: forall s. Eq s => Expr s Import -> Expr s Import
lint =  forall a b. ASetter a b a b -> (b -> Maybe a) -> a -> b
Dhall.Optics.rewriteOf forall (f :: * -> *) s a.
Applicative f =>
(Expr s a -> f (Expr s a)) -> Expr s a -> f (Expr s a)
subExpressions forall {s}. Expr s Import -> Maybe (Expr s Import)
rewrite
  where
    rewrite :: Expr s Import -> Maybe (Expr s Import)
rewrite Expr s Import
e =
            forall s a. Expr s a -> Maybe (Expr s a)
fixAssert                Expr s Import
e
        forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> forall a s. Eq a => Expr s a -> Maybe (Expr s a)
removeUnusedBindings     Expr s Import
e
        forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> forall {s}. Expr s Import -> Maybe (Expr s Import)
fixParentPath            Expr s Import
e
        forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> forall s a. Expr s a -> Maybe (Expr s a)
removeLetInLet           Expr s Import
e
        forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> forall {s}. Expr s Import -> Maybe (Expr s Import)
addPreludeExtensions     Expr s Import
e

-- | Remove unused `Let` bindings.
removeUnusedBindings :: Eq a => Expr s a -> Maybe (Expr s a)
-- Don't remove assertions!
removeUnusedBindings :: forall a s. Eq a => Expr s a -> Maybe (Expr s a)
removeUnusedBindings (Let (Binding Maybe s
_ Text
_ Maybe s
_ Maybe (Maybe s, Expr s a)
_ Maybe s
_ Expr s a
e) Expr s a
_)
    | forall s a. Expr s a -> Bool
isOrContainsAssert Expr s a
e = forall a. Maybe a
Nothing
removeUnusedBindings (Let (Binding Maybe s
_ Text
a Maybe s
_ Maybe (Maybe s, Expr s a)
_ Maybe s
_ Expr s a
_) Expr s a
d)
    | Bool -> Bool
not (Text -> Int -> Var
V Text
a Int
0 forall a s. Eq a => Var -> Expr s a -> Bool
`Core.freeIn` Expr s a
d) =
        forall a. a -> Maybe a
Just (forall s a. Int -> Var -> Expr s a -> Expr s a
Core.shift (-Int
1) (Text -> Int -> Var
V Text
a Int
0) Expr s a
d)
removeUnusedBindings Expr s a
_ = forall a. Maybe a
Nothing

-- | Fix `Let` bindings  that the user probably meant to be @assert@s
fixAssert :: Expr s a -> Maybe (Expr s a)
fixAssert :: forall s a. Expr s a -> Maybe (Expr s a)
fixAssert (Let (Binding { value :: forall s a. Binding s a -> Expr s a
value = v :: Expr s a
v@(forall s a. Expr s a -> Expr s a
Core.shallowDenote -> Equivalent {}), Maybe s
Maybe (Maybe s, Expr s a)
Text
bindingSrc2 :: forall s a. Binding s a -> Maybe s
annotation :: forall s a. Binding s a -> Maybe (Maybe s, Expr s a)
bindingSrc1 :: forall s a. Binding s a -> Maybe s
variable :: forall s a. Binding s a -> Text
bindingSrc0 :: forall s a. Binding s a -> Maybe s
bindingSrc2 :: Maybe s
annotation :: Maybe (Maybe s, Expr s a)
bindingSrc1 :: Maybe s
variable :: Text
bindingSrc0 :: Maybe s
..}) Expr s a
body) =
    forall a. a -> Maybe a
Just (forall s a. Binding s a -> Expr s a -> Expr s a
Let (Binding { value :: Expr s a
value = forall s a. Expr s a -> Expr s a
Assert Expr s a
v, Maybe s
Maybe (Maybe s, Expr s a)
Text
bindingSrc2 :: Maybe s
annotation :: Maybe (Maybe s, Expr s a)
bindingSrc1 :: Maybe s
variable :: Text
bindingSrc0 :: Maybe s
bindingSrc2 :: Maybe s
annotation :: Maybe (Maybe s, Expr s a)
bindingSrc1 :: Maybe s
variable :: Text
bindingSrc0 :: Maybe s
.. }) Expr s a
body)
fixAssert (Let Binding s a
binding body :: Expr s a
body@(forall s a. Expr s a -> Expr s a
Core.shallowDenote -> Equivalent {})) =
    forall a. a -> Maybe a
Just (forall s a. Binding s a -> Expr s a -> Expr s a
Let Binding s a
binding (forall s a. Expr s a -> Expr s a
Assert Expr s a
body))
fixAssert Expr s a
_ =
    forall a. Maybe a
Nothing

-- | This transforms @.\/..\/foo@ into @..\/foo@
fixParentPath :: Expr s Import -> Maybe (Expr s Import)
fixParentPath :: forall {s}. Expr s Import -> Maybe (Expr s Import)
fixParentPath (Embed Import
oldImport) = do
    let Import{ImportHashed
ImportMode
importMode :: Import -> ImportMode
importHashed :: Import -> ImportHashed
importMode :: ImportMode
importHashed :: ImportHashed
..} = Import
oldImport

    let ImportHashed{Maybe SHA256Digest
ImportType
importType :: ImportHashed -> ImportType
hash :: ImportHashed -> Maybe SHA256Digest
importType :: ImportType
hash :: Maybe SHA256Digest
..} = ImportHashed
importHashed

    case ImportType
importType of
        Local FilePrefix
Here File{ directory :: File -> Directory
directory = Directory { [Text]
components :: Directory -> [Text]
components :: [Text]
components }, Text
file :: File -> Text
file :: Text
.. }
            | Just NonEmpty Text
nonEmpty <- forall a. [a] -> Maybe (NonEmpty a)
NonEmpty.nonEmpty [Text]
components
            , forall a. NonEmpty a -> a
NonEmpty.last NonEmpty Text
nonEmpty forall a. Eq a => a -> a -> Bool
== Text
".." -> do
                let newDirectory :: Directory
newDirectory =
                        Directory { components :: [Text]
components = forall a. NonEmpty a -> [a]
NonEmpty.init NonEmpty Text
nonEmpty }

                let newImportType :: ImportType
newImportType =
                        FilePrefix -> File -> ImportType
Local FilePrefix
Parent File{ directory :: Directory
directory = Directory
newDirectory, Text
file :: Text
file :: Text
.. }

                let newImportHashed :: ImportHashed
newImportHashed =
                        ImportHashed { importType :: ImportType
importType = ImportType
newImportType, Maybe SHA256Digest
hash :: Maybe SHA256Digest
hash :: Maybe SHA256Digest
.. }

                let newImport :: Import
newImport = Import { importHashed :: ImportHashed
importHashed = ImportHashed
newImportHashed, ImportMode
importMode :: ImportMode
importMode :: ImportMode
.. }

                forall a. a -> Maybe a
Just (forall s a. a -> Expr s a
Embed Import
newImport)
        ImportType
_ ->
            forall a. Maybe a
Nothing
fixParentPath Expr s Import
_  = forall a. Maybe a
Nothing

{-| This transforms @https://prelude.dhall-lang.org/…/foo@ to
    @https://prelude.dhall-lang.org/…/foo.dhall@
-}
addPreludeExtensions :: Expr s Import -> Maybe (Expr s Import)
addPreludeExtensions :: forall {s}. Expr s Import -> Maybe (Expr s Import)
addPreludeExtensions (Embed Import
oldImport) = do
    let Import{ importHashed :: Import -> ImportHashed
importHashed = ImportHashed
oldImportHashed, ImportMode
importMode :: ImportMode
importMode :: Import -> ImportMode
.. } = Import
oldImport

    let ImportHashed{ importType :: ImportHashed -> ImportType
importType = ImportType
oldImportType, Maybe SHA256Digest
hash :: Maybe SHA256Digest
hash :: ImportHashed -> Maybe SHA256Digest
.. } = ImportHashed
oldImportHashed

    case ImportType
oldImportType of
        Remote URL{ path :: URL -> File
path = File
oldPath, Maybe Text
Maybe (Expr Src Import)
Text
Scheme
headers :: URL -> Maybe (Expr Src Import)
query :: URL -> Maybe Text
authority :: URL -> Text
scheme :: URL -> Scheme
headers :: Maybe (Expr Src Import)
query :: Maybe Text
authority :: Text
scheme :: Scheme
..}
            | Text
authority forall a. Eq a => a -> a -> Bool
== Text
"prelude.dhall-lang.org" ->
                case File
oldPath of
                    File{ file :: File -> Text
file = Text
oldFile, Directory
directory :: Directory
directory :: File -> Directory
.. }
                        | Bool -> Bool
not (Text -> Text -> Bool
Text.isSuffixOf Text
".dhall" Text
oldFile) -> do
                            let newFile :: Text
newFile = Text
oldFile forall a. Semigroup a => a -> a -> a
<> Text
".dhall"

                            let newPath :: File
newPath = File{ file :: Text
file = Text
newFile, Directory
directory :: Directory
directory :: Directory
.. }

                            let newImportType :: ImportType
newImportType = URL -> ImportType
Remote URL{ path :: File
path = File
newPath, Maybe Text
Maybe (Expr Src Import)
Text
Scheme
headers :: Maybe (Expr Src Import)
query :: Maybe Text
authority :: Text
scheme :: Scheme
headers :: Maybe (Expr Src Import)
query :: Maybe Text
authority :: Text
scheme :: Scheme
.. }

                            let newImportHashed :: ImportHashed
newImportHashed =
                                    ImportHashed{ importType :: ImportType
importType = ImportType
newImportType, Maybe SHA256Digest
hash :: Maybe SHA256Digest
hash :: Maybe SHA256Digest
.. }

                            let newImport :: Import
newImport =
                                    Import{ importHashed :: ImportHashed
importHashed = ImportHashed
newImportHashed, ImportMode
importMode :: ImportMode
importMode :: ImportMode
.. }

                            forall (m :: * -> *) a. Monad m => a -> m a
return (forall s a. a -> Expr s a
Embed Import
newImport)
                    File
_ ->
                        forall a. Maybe a
Nothing
        ImportType
_ -> do
            forall a. Maybe a
Nothing
addPreludeExtensions Expr s Import
_ = forall a. Maybe a
Nothing

isOrContainsAssert :: Expr s a -> Bool
isOrContainsAssert :: forall s a. Expr s a -> Bool
isOrContainsAssert (Assert Expr s a
_) = Bool
True
isOrContainsAssert Expr s a
e = forall s t a b. FoldLike Any s t a b -> (a -> Bool) -> s -> Bool
Lens.Family.anyOf forall (f :: * -> *) s a.
Applicative f =>
(Expr s a -> f (Expr s a)) -> Expr s a -> f (Expr s a)
subExpressions forall s a. Expr s a -> Bool
isOrContainsAssert Expr s a
e

-- | The difference between
--
-- > let x = 1 let y = 2 in x + y
--
-- and
--
-- > let x = 1 in let y = 2 in x + y
--
-- is that in the second expression, the inner 'Let' is wrapped by a 'Note'.
--
-- We remove such a 'Note' in order to consolidate nested let-blocks into a
-- single one.
removeLetInLet :: Expr s a -> Maybe (Expr s a)
removeLetInLet :: forall s a. Expr s a -> Maybe (Expr s a)
removeLetInLet (Let Binding s a
binding (Note s
_ l :: Expr s a
l@Let{})) = forall a. a -> Maybe a
Just (forall s a. Binding s a -> Expr s a -> Expr s a
Let Binding s a
binding Expr s a
l)
removeLetInLet Expr s a
_ = forall a. Maybe a
Nothing

-- | This replaces a record of key-value pairs with the equivalent use of
--   @toMap@
--
-- This is currently not used by @dhall lint@ because this would sort @Map@
-- keys, which is not necessarily a behavior-preserving change, but is still
-- made available as a convenient rewrite rule.  For example,
-- @{json,yaml}-to-dhall@ use this rewrite to simplify their output.
useToMap :: Expr s a -> Maybe (Expr s a)
useToMap :: forall s a. Expr s a -> Maybe (Expr s a)
useToMap
    (ListLit
        t :: Maybe (Expr s a)
t@(Just
            (forall s a. Expr s a -> Expr s a
Core.shallowDenote -> App
                (forall s a. Expr s a -> Expr s a
Core.shallowDenote -> Expr s a
List)
                (forall s a. Expr s a -> Expr s a
Core.shallowDenote -> Record
                    (forall k v. Map k v -> Map k v
Dhall.Map.sort ->
                        [ (Text
"mapKey", forall s a. Expr s a -> Expr s a
Core.shallowDenote forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall s a. RecordField s a -> Expr s a
Core.recordFieldValue -> Expr s a
Text)
                        , (Text
"mapValue", RecordField s a
_)
                        ]
                    )
                )
            )
        )
        []
    ) =
        forall a. a -> Maybe a
Just (forall s a. Expr s a -> Maybe (Expr s a) -> Expr s a
ToMap (forall s a. Map Text (RecordField s a) -> Expr s a
RecordLit []) Maybe (Expr s a)
t)
useToMap (ListLit Maybe (Expr s a)
_ Seq (Expr s a)
keyValues)
    | Bool -> Bool
not (forall (t :: * -> *) a. Foldable t => t a -> Bool
null Seq (Expr s a)
keyValues)
    , Just Seq (Text, RecordField s a)
keyValues' <- forall (t :: * -> *) (f :: * -> *) a b.
(Traversable t, Applicative f) =>
(a -> f b) -> t a -> f (t b)
traverse forall {s} {a}. Expr s a -> Maybe (Text, RecordField s a)
convert Seq (Expr s a)
keyValues =
        forall a. a -> Maybe a
Just
            (forall s a. Expr s a -> Maybe (Expr s a) -> Expr s a
ToMap
                (forall s a. Map Text (RecordField s a) -> Expr s a
RecordLit (forall k v. Ord k => [(k, v)] -> Map k v
Dhall.Map.fromList (forall (t :: * -> *) a. Foldable t => t a -> [a]
Foldable.toList Seq (Text, RecordField s a)
keyValues')))
                forall a. Maybe a
Nothing
            )
  where
    convert :: Expr s a -> Maybe (Text, RecordField s a)
convert Expr s a
keyValue =
        case forall s a. Expr s a -> Expr s a
Core.shallowDenote Expr s a
keyValue of
            RecordLit
                (forall k v. Map k v -> Map k v
Dhall.Map.sort ->
                    [ (Text
"mapKey"  , forall s a. Expr s a -> Expr s a
Core.shallowDenote forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall s a. RecordField s a -> Expr s a
Core.recordFieldValue -> TextLit (Chunks [] Text
key))
                    , (Text
"mapValue", RecordField s a
value)
                    ]
                ) ->
                    forall a. a -> Maybe a
Just (Text
key, RecordField s a
value)
            Expr s a
_ ->
                forall a. Maybe a
Nothing
useToMap Expr s a
_ =
    forall a. Maybe a
Nothing