{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms   #-}
{-# LANGUAGE Strict            #-}
module Tokstyle.Linter.Booleans (descr) where

import           Control.Monad.State.Strict  (State)
import qualified Control.Monad.State.Strict  as State
import           Data.Fix                    (Fix (..))
import           Data.Text                   (Text)
import qualified Data.Text                   as Text
import           Language.Cimple             (Lexeme, LiteralType (..), Node,
                                              NodeF (..))
import           Language.Cimple.Diagnostics (CimplePos, Diagnostic)
import           Language.Cimple.TraverseAst (AstActions, astActions, doNode,
                                              traverseAst)
import           Tokstyle.Common             (warn)

pattern ReturnBool, OnlyReturnBool :: Node a
-- | `return true` or `return false`.
pattern $mReturnBool :: forall r a. Node a -> (Void# -> r) -> (Void# -> r) -> r
ReturnBool <- Fix (Return (Just (Fix (LiteralExpr Bool _))))
-- | A compound statement with only a return true/false in it.
pattern $mOnlyReturnBool :: forall r a. Node a -> (Void# -> r) -> (Void# -> r) -> r
OnlyReturnBool <- Fix (CompoundStmt [ReturnBool])

checkStmts :: FilePath -> [Node (Lexeme Text)] -> State [Diagnostic CimplePos] ()
checkStmts :: FilePath -> [Node (Lexeme Text)] -> State [Diagnostic CimplePos] ()
checkStmts FilePath
_ [] = () -> State [Diagnostic CimplePos] ()
forall (m :: * -> *) a. Monad m => a -> m a
return ()
checkStmts FilePath
file [s :: Node (Lexeme Text)
s@(Fix (IfStmt Node (Lexeme Text)
_ Node (Lexeme Text)
OnlyReturnBool Maybe (Node (Lexeme Text))
Nothing)), Node (Lexeme Text)
ReturnBool] =
    FilePath
-> Node (Lexeme Text) -> Text -> State [Diagnostic CimplePos] ()
forall diags at.
(HasDiagnosticsRich diags CimplePos,
 HasDiagnosticInfo at CimplePos) =>
FilePath -> at -> Text -> DiagnosticsT diags ()
warn FilePath
file Node (Lexeme Text)
s Text
"if-statement followed by boolean return can be simplified to return"
checkStmts FilePath
file [s :: Node (Lexeme Text)
s@(Fix (IfStmt Node (Lexeme Text)
_ Node (Lexeme Text)
OnlyReturnBool (Just Node (Lexeme Text)
OnlyReturnBool)))] =
    FilePath
-> Node (Lexeme Text) -> Text -> State [Diagnostic CimplePos] ()
forall diags at.
(HasDiagnosticsRich diags CimplePos,
 HasDiagnosticInfo at CimplePos) =>
FilePath -> at -> Text -> DiagnosticsT diags ()
warn FilePath
file Node (Lexeme Text)
s Text
"if/else with return true/false can be simplified to return"
checkStmts FilePath
file (Node (Lexeme Text)
_:[Node (Lexeme Text)]
ss) = FilePath -> [Node (Lexeme Text)] -> State [Diagnostic CimplePos] ()
checkStmts FilePath
file [Node (Lexeme Text)]
ss


linter :: AstActions (State [Diagnostic CimplePos]) Text
linter :: AstActions (State [Diagnostic CimplePos]) Text
linter = AstActions (State [Diagnostic CimplePos]) Text
forall (f :: * -> *) text. Applicative f => AstActions f text
astActions
    { doNode :: FilePath
-> Node (Lexeme Text)
-> State [Diagnostic CimplePos] ()
-> State [Diagnostic CimplePos] ()
doNode = \FilePath
file Node (Lexeme Text)
node State [Diagnostic CimplePos] ()
act ->
        case Node (Lexeme Text) -> NodeF (Lexeme Text) (Node (Lexeme Text))
forall (f :: * -> *). Fix f -> f (Fix f)
unFix Node (Lexeme Text)
node of
            BinaryExpr (Fix (LiteralExpr LiteralType
Bool Lexeme Text
_)) BinaryOp
_ Node (Lexeme Text)
_ -> FilePath
-> Node (Lexeme Text) -> Text -> State [Diagnostic CimplePos] ()
forall diags at.
(HasDiagnosticsRich diags CimplePos,
 HasDiagnosticInfo at CimplePos) =>
FilePath -> at -> Text -> DiagnosticsT diags ()
warn FilePath
file Node (Lexeme Text)
node Text
message
            BinaryExpr Node (Lexeme Text)
_ BinaryOp
_ (Fix (LiteralExpr LiteralType
Bool Lexeme Text
_)) -> FilePath
-> Node (Lexeme Text) -> Text -> State [Diagnostic CimplePos] ()
forall diags at.
(HasDiagnosticsRich diags CimplePos,
 HasDiagnosticInfo at CimplePos) =>
FilePath -> at -> Text -> DiagnosticsT diags ()
warn FilePath
file Node (Lexeme Text)
node Text
message
            CompoundStmt [Node (Lexeme Text)]
stmts -> do
                FilePath -> [Node (Lexeme Text)] -> State [Diagnostic CimplePos] ()
checkStmts FilePath
file [Node (Lexeme Text)]
stmts
                State [Diagnostic CimplePos] ()
act
            NodeF (Lexeme Text) (Node (Lexeme Text))
_ -> State [Diagnostic CimplePos] ()
act
    }
  where
    message :: Text
message = Text
"boolean constants should not appear in binary expressions (use ! for negation)"

analyse :: (FilePath, [Node (Lexeme Text)]) -> [Diagnostic CimplePos]
analyse :: (FilePath, [Node (Lexeme Text)]) -> [Diagnostic CimplePos]
analyse = [Diagnostic CimplePos] -> [Diagnostic CimplePos]
forall a. [a] -> [a]
reverse ([Diagnostic CimplePos] -> [Diagnostic CimplePos])
-> ((FilePath, [Node (Lexeme Text)]) -> [Diagnostic CimplePos])
-> (FilePath, [Node (Lexeme Text)])
-> [Diagnostic CimplePos]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (State [Diagnostic CimplePos] ()
 -> [Diagnostic CimplePos] -> [Diagnostic CimplePos])
-> [Diagnostic CimplePos]
-> State [Diagnostic CimplePos] ()
-> [Diagnostic CimplePos]
forall a b c. (a -> b -> c) -> b -> a -> c
flip State [Diagnostic CimplePos] ()
-> [Diagnostic CimplePos] -> [Diagnostic CimplePos]
forall s a. State s a -> s -> s
State.execState [] (State [Diagnostic CimplePos] () -> [Diagnostic CimplePos])
-> ((FilePath, [Node (Lexeme Text)])
    -> State [Diagnostic CimplePos] ())
-> (FilePath, [Node (Lexeme Text)])
-> [Diagnostic CimplePos]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AstActions (State [Diagnostic CimplePos]) Text
-> (FilePath, [Node (Lexeme Text)])
-> State [Diagnostic CimplePos] ()
forall text a (f :: * -> *).
(TraverseAst text a, Applicative f) =>
AstActions f text -> a -> f ()
traverseAst AstActions (State [Diagnostic CimplePos]) Text
linter

descr :: ((FilePath, [Node (Lexeme Text)]) -> [Diagnostic CimplePos], (Text, Text))
descr :: ((FilePath, [Node (Lexeme Text)]) -> [Diagnostic CimplePos],
 (Text, Text))
descr = ((FilePath, [Node (Lexeme Text)]) -> [Diagnostic CimplePos]
analyse, (Text
"booleans", [Text] -> Text
Text.unlines
    [ Text
"Checks for if/else statements that return true/false and could be simplified to"
    , Text
"just return. E.g.:"
    , Text
""
    , Text
"```cpp"
    , Text
"bool foo(void) {"
    , Text
"  if (check_something()) {"
    , Text
"    return false;"
    , Text
"  }"
    , Text
"  return true;"
    , Text
"}"
    , Text
"```"
    , Text
""
    , Text
"could be simplified to:"
    , Text
""
    , Text
"```cpp"
    , Text
"bool foo(void) {"
    , Text
"  return !check_something();"
    , Text
"}"
    , Text
"```"
    , Text
""
    , Text
"Also checks for the use of `true` or `false` in binary expressions. E.g."
    , Text
"`a == true` should be `a` and `a != true` should be `!a`."
    , Text
""
    , Text
"**Reason:** simpler code is easier to read."
    ]))