packages feed

glualint-1.27.0: src/GLuaFixer/LintMessage.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module GLuaFixer.LintMessage where

import Control.Monad
import Data.Aeson
import Data.List (sortOn)
import Data.Maybe (isJust)
import GLua.AG.PrettyPrint (renderPSError, renderPos, renderRegion)
import GLua.AG.Token (Token)
import GLua.Position (LineColPos (..), Region (..))
import System.Environment (lookupEnv)
import Text.Parsec (ParseError)

-- | Output formats for logging
data LogFormat = StandardLogFormat | GithubLogFormat

data LogFormatChoice = AutoLogFormatChoice | LogFormatChoice !LogFormat

instance Show LogFormat where
  show StandardLogFormat = "standard"
  show GithubLogFormat = "github"

instance Show LogFormatChoice where
  show (LogFormatChoice choice) = show choice
  show AutoLogFormatChoice = "auto"

instance ToJSON LogFormat where
  toJSON StandardLogFormat = "standard"
  toJSON GithubLogFormat = "github"

instance ToJSON LogFormatChoice where
  toJSON (LogFormatChoice choice) = toJSON choice
  toJSON AutoLogFormatChoice = "auto"

instance FromJSON LogFormatChoice where
  parseJSON (String logFormat) = case logFormat of
    "standard" -> pure $ LogFormatChoice StandardLogFormat
    "github" -> pure $ LogFormatChoice GithubLogFormat
    "auto" -> pure AutoLogFormatChoice
    _ -> fail ("Please use either \"auto\" \"standard\" or \"github\" but was " ++ show logFormat)
  parseJSON _ = mzero

data Severity = LintWarning | LintError
  deriving (Eq)

-- | With the Space(Before|After)(Parenthesis|Bracket|Brace), it depends on the pretty print
-- settings whether the space is desired or not. This encodes what we ask the user to do.
data RemoveOrAddSpace
  = RemoveSpace
  | AddSpace
  deriving (Eq)

-- | Representation of the different kinds of issues that can be raised. Many of the arguments are
-- 'String', because this data type is a rewrite of what was previously directly rendered Strings.
-- Many of these Strings can later be rewritten to their own types if necessary.
data Issue
  = IssueParseError ParseError
  | -- From BadSequenceFinder

    -- | Reason
    Deprecated !String
  | Profanity
  | -- | message
    BeginnerMistake !String
  | -- | message
    WhitespaceStyle !String
  | SpaceAfterParenthesis !RemoveOrAddSpace
  | SpaceBeforeParenthesis !RemoveOrAddSpace
  | SpaceAfterBracket !RemoveOrAddSpace
  | SpaceBeforeBracket !RemoveOrAddSpace
  | SpaceAfterBrace !RemoveOrAddSpace
  | SpaceBeforeBrace !RemoveOrAddSpace
  | SpaceAfterComma !RemoveOrAddSpace
  | SpaceBeforeComma !RemoveOrAddSpace
  | -- Issues found in the lexicon (see LexLint.ag)
    TrailingWhitespace
  | InconsistentTabsSpaces
  | SyntaxInconsistency
      !String
      -- ^ First encountered
      !String
      -- ^ Second encountered
  | -- Line length limit (see LineLimitParser.hs)
    LineTooLong
  | -- Issues found in the AST (see ASTLint.ag)
    VariableShadows
      !String
      -- ^ Name of the variable being shadowed
      !Region
      -- ^ Definition location of variable being shadowed
  | GotoAsIdentifier
  | InconsistentVariableNaming
  | ScopePyramids
  | -- | Variable name
    UnusedVariable !String
  | AvoidGoto
  | EmptyDoBlock
  | EmptyWhileLoop
  | EmptyRepeat
  | EmptyIf
  | DoubleIf
  | EmptyFor
  | EmptyElseIf
  | EmptyElse
  | SelfInNonMeta
  | SelfEntity
  | SelfWeapon
  | UnnecessaryParentheses
  | -- | Alternative to using the negation
    SillyNegation !String
  | -- | The key that is duplicated
    DuplicateKeyInTable !Token
  deriving (Eq)

-- | Represents lint messages
data LintMessage = LintMessage
  { lintmsg_severity :: !Severity
  , lintmsg_region :: !Region
  , lintmsg_message :: !Issue
  , lintmsg_file :: !FilePath
  }
  deriving (Eq)

instance Show LintMessage where
  show = formatLintMessageDefault

issueDescription :: Issue -> String
issueDescription = \case
  IssueParseError parseError -> renderPSError parseError
  Deprecated reason -> "Deprecated: " ++ reason
  Profanity -> "Watch your profanity"
  BeginnerMistake msg -> msg
  WhitespaceStyle msg -> "Style: " ++ msg
  SpaceAfterParenthesis RemoveSpace -> "Style: Please remove the space after the parenthesis"
  SpaceAfterParenthesis AddSpace -> "Style: Please add a space after the parenthesis"
  SpaceBeforeParenthesis RemoveSpace -> "Style: Please remove the space before the parenthesis"
  SpaceBeforeParenthesis AddSpace -> "Style: Please add a space before the parenthesis"
  SpaceAfterBracket RemoveSpace -> "Style: Please remove the space after the bracket"
  SpaceAfterBracket AddSpace -> "Style: Please add a space after the bracket"
  SpaceBeforeBracket RemoveSpace -> "Style: Please remove the space before the bracket"
  SpaceBeforeBracket AddSpace -> "Style: Please add a space before the bracket"
  SpaceAfterBrace RemoveSpace -> "Style: Please remove the space after the brace"
  SpaceAfterBrace AddSpace -> "Style: Please add a space after the brace"
  SpaceBeforeBrace RemoveSpace -> "Style: Please remove the space before the brace"
  SpaceBeforeBrace AddSpace -> "Style: Please add a space before the brace"
  SpaceAfterComma RemoveSpace -> "Style: Please remove the space after the comma"
  SpaceAfterComma AddSpace -> "Style: Please add a space after the comma"
  SpaceBeforeComma RemoveSpace -> "Style: Please remove the space before the comma"
  SpaceBeforeComma AddSpace -> "Style: Please add a space before the comma"
  TrailingWhitespace -> "Trailing whitespace"
  InconsistentTabsSpaces -> "Inconsistent use of tabs and spaces for indentation"
  SyntaxInconsistency firstEncountered secondEncountered ->
    "Inconsistent use of '" ++ firstEncountered ++ "' and '" ++ secondEncountered ++ "'"
  LineTooLong -> "Style: Line too long"
  VariableShadows lbl (Region start _) ->
    "Variable '" ++ lbl ++ "' shadows existing binding, defined at " ++ renderPos start
  GotoAsIdentifier ->
    "Don't use 'goto' as an identifier, later versions of Lua will confuse it with the goto keyword."
  InconsistentVariableNaming ->
    "Inconsistent variable naming! There are variables that start with a lowercase letter, as well as ones that start with an uppercase letter. Please decide on one style."
  ScopePyramids ->
    "Are you Egyptian? What's with these fucking scope pyramids!?"
  UnusedVariable varName ->
    "Unused variable: " ++ varName
  AvoidGoto ->
    "Don't use labels and gotos unless you're jumping out of multiple loops."
  EmptyDoBlock -> "Empty do block"
  EmptyWhileLoop -> "Empty while loop"
  EmptyRepeat -> "Empty repeat statement"
  EmptyIf -> "Empty if statement"
  DoubleIf ->
    "Double if statement. Please combine the condition of this if statement with that of the outer if statement using `and`."
  EmptyFor -> "Empty for loop"
  EmptyElseIf -> "Empty elseif statement"
  EmptyElse -> "Empty else statement"
  SelfInNonMeta ->
    "Don't use self in a non-metafunction"
  SelfEntity ->
    "'self.Entity' is the same as just 'self' in SENTs"
  SelfWeapon ->
    "'self.Weapon' is the same as just 'self' in SWEPs"
  UnnecessaryParentheses -> "Unnecessary parentheses"
  SillyNegation alternative ->
    "Silly negation. Use '" ++ alternative ++ "'"
  DuplicateKeyInTable keyToken ->
    "Duplicate key in table: '" ++ show keyToken ++ "'."

-- | Shorthand title of an issue. Several issues may share the same title.
issueTitle :: Issue -> String
issueTitle = \case
  IssueParseError _ -> "Parse error"
  Deprecated _ -> "Deprecated"
  Profanity -> "Profanity"
  BeginnerMistake _ -> "Beginner mistake"
  WhitespaceStyle _ -> "Whitespace style"
  TrailingWhitespace -> "Trailing whitespace"
  SpaceAfterParenthesis _ -> "Space after parenthesis"
  SpaceBeforeParenthesis _ -> "Space before parenthesis"
  SpaceAfterBracket _ -> "Space after bracket"
  SpaceBeforeBracket _ -> "Space before bracket"
  SpaceAfterBrace _ -> "Space after brace"
  SpaceBeforeBrace _ -> "Space before brace"
  SpaceAfterComma _ -> "Space after comma"
  SpaceBeforeComma _ -> "Space before comma"
  InconsistentTabsSpaces -> "Syntax inconsistency"
  SyntaxInconsistency _ _ -> "Syntax inconsistency"
  LineTooLong -> "Line too long"
  VariableShadows _ _ -> "Shadowing"
  GotoAsIdentifier -> "Goto"
  InconsistentVariableNaming -> "Variable inconsistency"
  ScopePyramids -> "Scope depth"
  UnusedVariable _ -> "Unused variable"
  AvoidGoto -> "Goto"
  EmptyDoBlock -> "Empty block"
  EmptyWhileLoop -> "Empty block"
  EmptyRepeat -> "Empty block"
  EmptyIf -> "Empty block"
  DoubleIf -> "Double if-statement"
  EmptyFor -> "Empty block"
  EmptyElseIf -> "Empty block"
  EmptyElse -> "Empty block"
  SelfInNonMeta -> "Bad self"
  SelfEntity -> "Deprecated"
  SelfWeapon -> "Deprecated"
  UnnecessaryParentheses -> "Unnecessary parentheses"
  SillyNegation _ -> "Unnecessary negation"
  DuplicateKeyInTable _ -> "Duplicate key"

logFormatChoiceToLogFormat :: LogFormatChoice -> IO LogFormat
logFormatChoiceToLogFormat = \case
  LogFormatChoice format -> pure format
  AutoLogFormatChoice -> do
    actionsExists <- isJust <$> lookupEnv "GITHUB_ACTIONS"
    workflowExists <- isJust <$> lookupEnv "GITHUB_WORKFLOW"
    if actionsExists && workflowExists
      then pure GithubLogFormat
      else pure StandardLogFormat

formatLintMessage :: LogFormat -> LintMessage -> String
formatLintMessage StandardLogFormat lintMsg = formatLintMessageDefault lintMsg
formatLintMessage GithubLogFormat lintMsg = formatLintMessageGithub lintMsg

formatLintMessageDefault :: LintMessage -> String
formatLintMessageDefault (LintMessage severity region msg file) =
  let
    level = case severity of
      LintWarning -> "Warning"
      LintError -> "Error"
  in
    showString file
      . showString ": ["
      . showString level
      . showString "] "
      . showString (renderRegion region)
      . showString ": "
      . showString (issueDescription msg)
      $ ""

formatLintMessageGithub :: LintMessage -> String
formatLintMessageGithub (LintMessage severity (Region (LineColPos line col _) (LineColPos endLine endCol _)) msg file) =
  let
    level = case severity of
      LintWarning -> "warning"
      LintError -> "error"
  in
    showString "::"
      . showString level
      . showString " file="
      . showString file
      . showString ",line="
      . shows (succ line)
      . showString ",col="
      . shows (succ col)
      . showString ",endLine="
      . shows (succ endLine)
      . showString ",endColumn="
      . shows (succ endCol)
      . showString ",title="
      . shows (issueTitle msg)
      . showString "::"
      . showString (issueDescription msg)
      $ ""

-- | Sort lint messages on file and then region
sortLintMessages :: [LintMessage] -> [LintMessage]
sortLintMessages = sortOn (\(LintMessage _ rg _ f) -> (f, rg))