packages feed

cgrep-9.0.0: src/CGrep/Parser/Atom.hs

--
-- Copyright (c) 2013-2025 Nicola Bonelli <nicola@larthia.com>
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
--

module CGrep.Parser.Atom (
    Atom (..),
    mkAtomFromToken,
    findAllMatches,
    wildCardMap,
) where

import qualified Data.Map as M

import CGrep.Common (trimT)
import CGrep.Distance ((~==))
import CGrep.Parser.Char (isDigit)

import Data.List (tails)

import Options (
    Options (edit_dist, prefix_match, suffix_match, word_match),
 )
import Util (unquoteT)

import qualified CGrep.Parser.Token as T

import qualified Data.Text as T

data Atom
    = Any
    | Keyword
    | Number
    | Oct
    | Hex
    | String
    | Literal
    | Placeholder T.Text
    | Exact T.Token
    deriving stock (Eq, Ord, Show)

wildCardMap :: M.Map T.Text Atom
wildCardMap =
    M.fromList
        [ ("ANY", Any)
        , ("KEY", Keyword)
        , ("OCT", Oct)
        , ("HEX", Hex)
        , ("NUM", Number)
        , ("STR", String)
        , ("LIT", String)
        ]

mkAtomFromToken :: T.Token -> Atom
mkAtomFromToken t
    | T.isTokenIdentifier t = case () of
        _
            | Just wc <- M.lookup txt wildCardMap -> wc
            | isAtomPlaceholder txt -> Placeholder txt
            | otherwise -> Exact $ T.mkTokenIdentifier (unescapeAtom txt)
          where
            txt = T.tToken t
    | otherwise = Exact t

{-# INLINE isAtomPlaceholder #-}
isAtomPlaceholder :: T.Text -> Bool
isAtomPlaceholder s =
    if
        | Just (x, T.uncons -> Just (y, _)) <- T.uncons s -> wprefix x && isDigit y
        | Just (x, "") <- T.uncons s -> wprefix x
        | otherwise -> errorWithoutStackTrace "CGrep: isAtomIdentifier"
  where
    wprefix x = x == '$' || x == '_'

unescapeAtom :: T.Text -> T.Text
unescapeAtom (T.uncons -> Just ('$', xs)) = xs
unescapeAtom (T.uncons -> Just ('_', xs)) = xs
unescapeAtom xs = xs
{-# INLINE unescapeAtom #-}

findAllMatches :: Options -> [[Atom]] -> [T.Token] -> [T.Token]
findAllMatches opt ws ts = concatMap (\w -> findAllMatches' opt w ts) ws
{-# INLINE findAllMatches #-}

findAllMatches' :: Options -> [Atom] -> [T.Token] -> [T.Token]
findAllMatches' opt as ts =
    let indices = findIndicesBy (doesAtomMatchToken opt) as ts
     in concatMap
            ( \i ->
                let s = extractSlice i (length as) ts
                 in if atomsCheckOccurrences as s
                        then s
                        else []
            )
            indices

extractSlice :: Int -> Int -> [a] -> [a]
extractSlice i n xs = take n (drop i xs)
{-# INLINE extractSlice #-}

-- The pattern _ matches any token, whereas _1, _2, etc. match tokens that must be equal across their respective occurrences.

atomsCheckOccurrences :: [Atom] -> [T.Token] -> Bool
atomsCheckOccurrences as ts = M.foldr checkAndFold True (m)
  where
    checkAndFold xs acc =
        acc && case xs of
            [] -> True
            (y : ys) -> all (== y) ys
    m =
        M.mapWithKey
            ( \k xs ->
                case k of
                    Placeholder "_0" -> xs
                    Placeholder "_1" -> xs
                    Placeholder "_2" -> xs
                    Placeholder "_3" -> xs
                    Placeholder "_4" -> xs
                    Placeholder "_5" -> xs
                    Placeholder "_6" -> xs
                    Placeholder "_7" -> xs
                    Placeholder "_8" -> xs
                    Placeholder "_9" -> xs
                    _ -> []
            )
            $ M.fromListWith (<>)
            $ zip as (take (length as) (map (: []) ts))

doesAtomMatchToken :: Options -> Atom -> T.Token -> Bool
doesAtomMatchToken opt (Exact l) r
    | T.isTokenIdentifier l && T.isTokenIdentifier r =
        if
            | word_match opt -> T.tToken l == T.tToken r
            | prefix_match opt -> T.tToken l `T.isPrefixOf` T.tToken r
            | suffix_match opt -> T.tToken l `T.isSuffixOf` T.tToken r
            | edit_dist opt -> (T.unpack . T.tToken) l ~== T.unpack (T.tToken r)
            | otherwise -> T.tToken l `T.isInfixOf` T.tToken r
    | T.isTokenString l && T.isTokenString r =
        if
            | word_match opt -> ls == rs
            | prefix_match opt -> ls `T.isPrefixOf` rs
            | suffix_match opt -> ls `T.isSuffixOf` rs
            | edit_dist opt -> T.unpack ls ~== T.unpack rs
            | otherwise -> ls `T.isInfixOf` rs
    | otherwise = l `T.eqToken` r
  where
    ls = (unquoteT . trimT) (T.tToken l)
    rs = (unquoteT . trimT) (T.tToken r)
doesAtomMatchToken _ Any _ = True
doesAtomMatchToken _ (Placeholder _) t = T.isTokenIdentifier t
doesAtomMatchToken _ Keyword t = T.isTokenKeyword t
doesAtomMatchToken _ String t = T.isTokenString t
doesAtomMatchToken _ Literal t = T.isTokenString t
doesAtomMatchToken _ Number t = T.isTokenNumber t
doesAtomMatchToken _ Oct t = T.isTokenNumber t && case T.uncons (T.tToken t) of Just ('0', T.uncons -> Just (d, _)) -> isDigit d; _ -> False
doesAtomMatchToken _ Hex t = T.isTokenNumber t && case T.uncons (T.tToken t) of Just ('0', T.uncons -> Just ('x', _)) -> True; _ -> False

isPrefixOfBy :: (a -> b -> Bool) -> [a] -> [b] -> Bool
isPrefixOfBy _ [] _ = True
isPrefixOfBy _ (_ : _) [] = False
isPrefixOfBy p (x : xs) (y : ys) = p x y && isPrefixOfBy p xs ys
{-# INLINEABLE isPrefixOfBy #-}

findIndicesBy :: (a -> b -> Bool) -> [a] -> [b] -> [Int]
findIndicesBy p needle haystack =
    [i | (i, tail') <- zip [0 ..] (tails haystack), isPrefixOfBy p needle tail']
{-# INLINE findIndicesBy #-}