packages feed

ai-agent-diff-patch-0.0.1.0: src/AIAgent/DiffPatch/Unified/ProjectedContext/Parser.hs

{-# LANGUAGE OverloadedStrings #-}

-- | ProjectedContext layer: unified diff parser.
-- Contains the Megaparsec-based parser for unified diff format.
-- All functions are pure; no IO is performed in this module.
module AIAgent.DiffPatch.Unified.ProjectedContext.Parser
  ( Parser
  , parsePatch
  , pPatch
  , pFileHeader
  , pHunk
  , pHunkHeader
  , pDiffLine
  , pRestOfLine
  ) where

import qualified Data.Text as T
import Data.Text (Text)
import AIAgent.DiffPatch.Unified.CoreModel.Type

-- megaparsec imports
import Data.Void (Void)
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L


type Parser = Parsec Void Text

-- | Parse a unified diff Text into a Patch.
-- Accepts both the headed format ("--- ..." / "+++ ..." lines present) and
-- the bare format (starting directly with "@@ ... @@").
-- Returns InvalidPatch on parse failure.
parsePatch :: Text -> Either PatchError Patch
parsePatch input =
  case runParser pPatch "" input of
    Left err -> Left (InvalidPatch (T.pack (errorBundlePretty err)))
    Right p  -> Right p

-- | Top-level parser: optional file header, then one or more hunks.
pPatch :: Parser Patch
pPatch = do
  mHeader <- optional pFileHeader
  hunks   <- some pHunk
  eof
  return (Patch mHeader hunks)

-- | Parse the "--- old\n+++ new\n" file header.
pFileHeader :: Parser FileHeader
pFileHeader = do
  _      <- string "--- "
  oldPath <- pRestOfLine
  _      <- string "+++ "
  newPath <- pRestOfLine
  return (FileHeader oldPath newPath)

-- | Parse a single hunk: header line followed by diff lines.
pHunk :: Parser Hunk
pHunk = do
  (os, oc, ns, nc) <- pHunkHeader
  hlines           <- many pDiffLine
  return (Hunk os oc ns nc hlines)

-- | Parse "@@ -oldStart[,oldCount] +newStart[,newCount] @@[trailing]\n".
-- The count field is optional; when absent it defaults to 1.
pHunkHeader :: Parser (Int, Int, Int, Int)
pHunkHeader = do
  _ <- string "@@ -"
  os <- L.decimal
  oc <- option 1 (char ',' *> L.decimal)
  _ <- string " +"
  ns <- L.decimal
  nc <- option 1 (char ',' *> L.decimal)
  _ <- string " @@"
  _ <- pRestOfLine   -- consume optional trailing comment and the newline
  return (os, oc, ns, nc)

-- | Parse a single diff line: context (' '), removed ('-'), or added ('+').
-- As an extension for LLM-generated patch tolerance, a bare newline ('\n') with
-- no leading space is also accepted and treated as an empty context line.
-- The three standard prefixes are tried first; the bare-newline branch is a fallback.
pDiffLine :: Parser Line
pDiffLine =
      (char ' ' *> (Line Context <$> pRestOfLine))
  <|> (char '-' *> (Line Removed <$> pRestOfLine))
  <|> (char '+' *> (Line Added   <$> pRestOfLine))
  <|> (char '\n' *> return (Line Context ""))
  -- ^ Empty context line (LLM-generated patch tolerance).
  --   The Unified Diff spec requires " \n" but LLMs often emit bare "\n".
  --   Placed last so the three standard prefixes are tried first.

-- | Consume the rest of the current line (not including the newline) and
-- then consume the newline itself.  Returns the line content without '\n'.
pRestOfLine :: Parser Text
pRestOfLine = do
  content <- T.pack <$> manyTill anySingle (char '\n')
  return content