packages feed

hlint-3.6: src/Hint/NumLiteral.hs

{-
    Suggest the usage of underscore when NumericUnderscores is enabled.

<TEST>
123456
{-# LANGUAGE NumericUnderscores #-} \
1234
{-# LANGUAGE NumericUnderscores #-} \
12345 -- @Suggestion 12_345 @NoRefactor
{-# LANGUAGE NumericUnderscores #-} \
123456789.0441234e-123456 -- @Suggestion 123_456_789.044_123_4e-123_456 @NoRefactor
{-# LANGUAGE NumericUnderscores #-} \
0x12abc.523defp+172345 -- @Suggestion 0x1_2abc.523d_efp+172_345 @NoRefactor
{-# LANGUAGE NumericUnderscores #-} \
3.14159265359 -- @Suggestion 3.141_592_653_59 @NoRefactor
{-# LANGUAGE NumericUnderscores #-} \
12_33574_56
</TEST>

-}

module Hint.NumLiteral (numLiteralHint) where

import GHC.Hs
import GHC.LanguageExtensions.Type (Extension (..))
import GHC.Types.SrcLoc
import GHC.Types.SourceText
import GHC.Util.ApiAnnotation (extensions)
import Data.Char (isDigit, isOctDigit, isHexDigit)
import Data.List (intercalate)
import Data.Set (union)
import Data.Generics.Uniplate.DataOnly (universeBi)
import Refact.Types

import Hint.Type (DeclHint, toSSA, modComments, firstDeclComments)
import Idea (Idea, suggest)

numLiteralHint :: DeclHint
numLiteralHint _ modu =
  -- Comments appearing without an empty line before the first
  -- declaration in a module are now associated with the declaration
  -- not the module so to be safe, look also at `firstDeclComments
  -- modu` (https://gitlab.haskell.org/ghc/ghc/-/merge_requests/9517).
  let exts = union (extensions (modComments modu)) (extensions (firstDeclComments modu)) in
  if NumericUnderscores `elem` exts then
     concatMap suggestUnderscore . universeBi
  else
     const []

suggestUnderscore :: LHsExpr GhcPs -> [Idea]
suggestUnderscore x@(L _ (HsOverLit _ ol@(OverLit _ (HsIntegral intLit@(IL (SourceText srcTxt) _ _))))) =
  [ suggest "Use underscore" (reLoc x) (reLoc y) [r] | '_' `notElem` srcTxt, srcTxt /= underscoredSrcTxt ]
  where
    underscoredSrcTxt = addUnderscore srcTxt
    y :: LocatedAn an (HsExpr GhcPs)
    y = noLocA $ HsOverLit EpAnnNotUsed $ ol{ol_val = HsIntegral intLit{il_text = SourceText underscoredSrcTxt}}
    r = Replace Expr (toSSA x) [("a", toSSA y)] "a"
suggestUnderscore x@(L _ (HsOverLit _ ol@(OverLit _ (HsFractional fracLit@(FL (SourceText srcTxt) _ _ _ _))))) =
  [ suggest "Use underscore" (reLoc x) (reLoc y) [r] | '_' `notElem` srcTxt, srcTxt /= underscoredSrcTxt ]
  where
    underscoredSrcTxt = addUnderscore srcTxt
    y :: LocatedAn an (HsExpr GhcPs)
    y = noLocA $ HsOverLit EpAnnNotUsed $ ol{ol_val = HsFractional fracLit{fl_text = SourceText underscoredSrcTxt}}
    r = Replace Expr (toSSA x) [("a", toSSA y)] "a"
suggestUnderscore _ = mempty

addUnderscore :: String -> String
addUnderscore intStr = numLitToStr underscoredNumLit
 where
   numLit = toNumLiteral intStr
   underscoredNumLit = numLit{ nl_intPart = underscoreFromRight chunkSize $ nl_intPart numLit
                             , nl_fracPart = underscore chunkSize $ nl_fracPart numLit
                             , nl_exp = underscoreFromRight 3 $ nl_exp numLit -- Exponential part is always decimal
                             }
   chunkSize = if null (nl_prefix numLit) then 3 else 4

   underscore chunkSize = intercalate "_" . chunk chunkSize
   underscoreFromRight chunkSize str
     | length str < 5 = str
     | otherwise = reverse . underscore chunkSize . reverse $ str
   chunk chunkSize [] = []
   chunk chunkSize xs = a:chunk chunkSize b where (a, b) = splitAt chunkSize xs

data NumLiteral = NumLiteral
  { nl_prefix :: String
  , nl_intPart :: String
  , nl_decSep :: String -- decimal separator
  , nl_fracPart :: String
  , nl_expSep :: String -- e, e+, e-, p, p+, p-
  , nl_exp :: String
  } deriving (Show, Eq)

toNumLiteral :: String -> NumLiteral
toNumLiteral str = case str of
  '0':'b':digits -> (afterPrefix isBinDigit digits){nl_prefix = "0b"}
  '0':'B':digits -> (afterPrefix isBinDigit digits){nl_prefix = "0B"}
  '0':'o':digits -> (afterPrefix isOctDigit digits){nl_prefix = "0o"}
  '0':'O':digits -> (afterPrefix isOctDigit digits){nl_prefix = "0O"}
  '0':'x':digits -> (afterPrefix isHexDigit digits){nl_prefix = "0x"}
  '0':'X':digits -> (afterPrefix isHexDigit digits){nl_prefix = "0X"}
  _              -> afterPrefix isDigit str
  where
    isBinDigit x = x == '0' || x == '1'

    afterPrefix isDigit str = (afterIntPart isDigit suffix){nl_intPart = intPart}
      where (intPart, suffix) = span isDigit str

    afterIntPart isDigit ('.':suffix) = (afterDecSep isDigit suffix){nl_decSep = "."}
    afterIntPart isDigit str = afterFracPart str

    afterDecSep isDigit str = (afterFracPart suffix){nl_fracPart = fracPart}
      where (fracPart, suffix) = span isDigit str

    afterFracPart str = NumLiteral "" "" "" "" expSep exp
      where (expSep, exp) = break isDigit str

numLitToStr :: NumLiteral -> String
numLitToStr (NumLiteral p ip ds fp es e) = p ++ ip ++ ds ++ fp ++ es ++ e