<commit_before>{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE PatternGuards #-}

module HsImport.ImportChange
   ( ImportChange(..)
   , importChanges
   ) where

import Data.Maybe
import Data.List (find, (\\))
import Lens.Micro
import qualified Language.Haskell.Exts as HS
import qualified Data.Attoparsec.Text as A
import HsImport.SymbolImport (SymbolImport(..), Symbol(..), symbol, isHiding, swapImport)
import HsImport.ModuleImport (ModuleImport(..))
import HsImport.ImportPos (matchingImports)
import HsImport.Utils
import HsImport.Types

-- | How the import declarations should be changed
data ImportChange
   = ReplaceImportAt SrcSpan ImportDecl -- ^ replace the import declaration at SrcSpan
   | AddImportAfter SrcLine ImportDecl  -- ^ add import declaration after SrcLine
   | AddImportAtEnd ImportDecl          -- ^ add import declaration at end of source file
   | FindImportPos ImportDecl           -- ^ search for an insert position for the import declaration
   | NoImportChange                     -- ^ no changes of the import declarations
   deriving (Show)


importChanges :: ModuleImport -> Maybe SymbolImport -> Module -> Either Error [ImportChange]
importChanges (ModuleImport moduleName False Nothing) Nothing hsModule =
   Right [importModule moduleName hsModule]

importChanges (ModuleImport moduleName False Nothing) (Just symbolImport) hsModule
   = sequenceA [importModuleWithSymbol moduleName symbolImport hsModule]

importChanges (ModuleImport moduleName qualified as) symbolImport hsModule =
   sequenceA
      [ maybe (Right NoImportChange)
              (\sym -> importModuleWithSymbol moduleName sym hsModule)
              symbolImport
      , if qualified
         then Right $ importQualifiedModule moduleName
                                            (fromMaybe moduleName as)
                                            hsModule
         else Right $ maybe
            NoImportChange
            (\asName -> importModuleAs moduleName asName hsModule)
            as
      ]


importModule :: String -> Module -> ImportChange
importModule moduleName module_
   | matching@(_:_) <- matchingImports moduleName (importDecls module_) =
      if any hasEntireModuleImported matching
         then NoImportChange
         else FindImportPos $ importDecl moduleName

   | not $ null (importDecls module_) =
      FindImportPos $ importDecl moduleName

   | otherwise =
      case srcLineForNewImport module_ of
           Just srcLine -> AddImportAfter srcLine (importDecl moduleName)
           Nothing      -> AddImportAtEnd (importDecl moduleName)


existingMatching :: [ImportDecl] -> String -> SymbolImport -> Either Error ImportChange
existingMatching matching moduleName symbolImport
   | Just impDecl <- find hasEntireModuleImported matching =
      -- There is a module import
      if isHiding symbolImport
         then
            -- We add a hiding clause, since we only want to hide a very specific symbol.
            Right $ ReplaceImportAt (srcSpan . HS.ann $ impDecl) (setSymbol impDecl symbolImport)

         else
            -- If we want to import a symbol, we dont have to, since it is already imported.
            Right $ NoImportChange

   | any (hasSymbols symbolImport) matching =
      -- The symbol we want to import/hide is already imported/hidden.
      Right $ NoImportChange
   | otherwise =
      case find (hasAnySymbols $ isHiding symbolImport) matching of
         Just impDecl ->
            -- There is a fitting import declaration to which we can add the symbol to.
            Right $ ReplaceImportAt (srcSpan . HS.ann $ impDecl) (addSymbol impDecl symbolImport)

         Nothing      ->
            -- The symbol is either not imported/hidden or another import
            -- hides/imports it.
            -- If something explictly imports the symbol, we remove it from the import list.
            -- If something explictly hides the symbol, we remove it from the hiding list.
            case find (hasSymbolsOverlap (swapImport symbolImport)) matching of
               Just impDecl -> do
                  -- There is a import declaration that imports/hides the symbol we want to hide/import.
                  symbolList <- removeSymbol impDecl symbolImport
                  Right $ ReplaceImportAt (srcSpan . HS.ann $ impDecl) symbolList

               Nothing ->
                  -- Symbol is not mentioned at all.
                  Right $ FindImportPos $ importDeclWithSymbol moduleName symbolImport

importModuleWithSymbol :: String -> SymbolImport -> Module -> Either Error ImportChange
importModuleWithSymbol moduleName symbolImport module_
   | matching@(_:_) <- matchingImports moduleName (importDecls module_) =
      -- There is already a matching import line
      existingMatching matching moduleName symbolImport

   | not $ null (importDecls module_) =
      Right $ FindImportPos $ importDeclWithSymbol moduleName symbolImport

   | otherwise =
      case srcLineForNewImport module_ of
           Just srcLine -> Right $ AddImportAfter srcLine (importDeclWithSymbol moduleName symbolImport)
           Nothing      -> Right $ AddImportAtEnd (importDeclWithSymbol moduleName symbolImport)

-- | Extend the spec list with the given symbol.
-- Might result in duplciates.
extendSpecList :: SymbolImport -> HS.ImportSpecList Annotation -> HS.ImportSpecList Annotation
extendSpecList symbolImport (HS.ImportSpecList annotation hid specs) =
   HS.ImportSpecList annotation hid (specs ++ [importSpec $ symbol symbolImport])

-- | Remove an element from the import list if it matches the symbol.
-- If the resulting spec list is empty afterwards, Nothing is returned to remove
-- the import list.
-- Removes duplicate imports.
removeSpecList
   :: SymbolImport
   -> HS.ImportSpecList Annotation
   -> Either Error (Maybe (HS.ImportSpecList Annotation))
removeSpecList symbolImport (HS.ImportSpecList annotation hid specs) =
   let specListRemovedSymbol =
             traverse (removeSymbols (symbol symbolImport)) specs
   in specListRemovedSymbol >>= \specList ->
      if null (catMaybes specList)
          then Right Nothing -- Remove the spec list if it is empty now
          else Right $ Just $ HS.ImportSpecList annotation hid (catMaybes specList)

 where
  removeSymbols :: Symbol -> ImportSpec -> Either Error (Maybe ImportSpec)
  removeSymbols (SomeOf symName _) t@(HS.IThingAll _ name) =
     if symName == nameString name
        then Left "Terrible error"
        else Right $ Just t

  removeSymbols (SomeOf symName names) t@(HS.IThingWith a hsSymName hsNames) =
     if symName == nameString hsSymName
        then Right $ Just (HS.IThingWith a hsSymName (removeFromList names hsNames))
        else Right $ Just t

  removeSymbols (AllOf symName) t@(HS.IThingWith a hsSymName _) =
      if symName == nameString hsSymName
         -- Remove all used constructors
         then Right $ Just (HS.IThingWith a hsSymName [])
         else Right $ Just t

  removeSymbols sym spec =
      if imports sym spec
         then Right $ Nothing<commit_msg>Add real error message<commit_after>{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE PatternGuards #-}

module HsImport.ImportChange
   ( ImportChange(..)
   , importChanges
   ) where

import Data.Maybe
import Data.List (find, (\\))
import Lens.Micro
import qualified Language.Haskell.Exts as HS
import qualified Data.Attoparsec.Text as A
import HsImport.SymbolImport (SymbolImport(..), Symbol(..), symbol, isHiding, swapImport)
import HsImport.ModuleImport (ModuleImport(..))
import HsImport.ImportPos (matchingImports)
import HsImport.Utils
import HsImport.Types

-- | How the import declarations should be changed
data ImportChange
   = ReplaceImportAt SrcSpan ImportDecl -- ^ replace the import declaration at SrcSpan
   | AddImportAfter SrcLine ImportDecl  -- ^ add import declaration after SrcLine
   | AddImportAtEnd ImportDecl          -- ^ add import declaration at end of source file
   | FindImportPos ImportDecl           -- ^ search for an insert position for the import declaration
   | NoImportChange                     -- ^ no changes of the import declarations
   deriving (Show)


importChanges :: ModuleImport -> Maybe SymbolImport -> Module -> Either Error [ImportChange]
importChanges (ModuleImport moduleName False Nothing) Nothing hsModule =
   Right [importModule moduleName hsModule]

importChanges (ModuleImport moduleName False Nothing) (Just symbolImport) hsModule
   = sequenceA [importModuleWithSymbol moduleName symbolImport hsModule]

importChanges (ModuleImport moduleName qualified as) symbolImport hsModule =
   sequenceA
      [ maybe (Right NoImportChange)
              (\sym -> importModuleWithSymbol moduleName sym hsModule)
              symbolImport
      , if qualified
         then Right $ importQualifiedModule moduleName
                                            (fromMaybe moduleName as)
                                            hsModule
         else Right $ maybe
            NoImportChange
            (\asName -> importModuleAs moduleName asName hsModule)
            as
      ]


importModule :: String -> Module -> ImportChange
importModule moduleName module_
   | matching@(_:_) <- matchingImports moduleName (importDecls module_) =
      if any hasEntireModuleImported matching
         then NoImportChange
         else FindImportPos $ importDecl moduleName

   | not $ null (importDecls module_) =
      FindImportPos $ importDecl moduleName

   | otherwise =
      case srcLineForNewImport module_ of
           Just srcLine -> AddImportAfter srcLine (importDecl moduleName)
           Nothing      -> AddImportAtEnd (importDecl moduleName)


existingMatching :: [ImportDecl] -> String -> SymbolImport -> Either Error ImportChange
existingMatching matching moduleName symbolImport
   | Just impDecl <- find hasEntireModuleImported matching =
      -- There is a module import
      if isHiding symbolImport
         then
            -- We add a hiding clause, since we only want to hide a very specific symbol.
            Right $ ReplaceImportAt (srcSpan . HS.ann $ impDecl) (setSymbol impDecl symbolImport)

         else
            -- If we want to import a symbol, we dont have to, since it is already imported.
            Right $ NoImportChange

   | any (hasSymbols symbolImport) matching =
      -- The symbol we want to import/hide is already imported/hidden.
      Right $ NoImportChange
   | otherwise =
      case find (hasAnySymbols $ isHiding symbolImport) matching of
         Just impDecl ->
            -- There is a fitting import declaration to which we can add the symbol to.
            Right $ ReplaceImportAt (srcSpan . HS.ann $ impDecl) (addSymbol impDecl symbolImport)

         Nothing      ->
            -- The symbol is either not imported/hidden or another import
            -- hides/imports it.
            -- If something explictly imports the symbol, we remove it from the import list.
            -- If something explictly hides the symbol, we remove it from the hiding list.
            case find (hasSymbolsOverlap (swapImport symbolImport)) matching of
               Just impDecl -> do
                  -- There is a import declaration that imports/hides the symbol we want to hide/import.
                  symbolList <- removeSymbol impDecl symbolImport
                  Right $ ReplaceImportAt (srcSpan . HS.ann $ impDecl) symbolList

               Nothing ->
                  -- Symbol is not mentioned at all.
                  Right $ FindImportPos $ importDeclWithSymbol moduleName symbolImport

importModuleWithSymbol :: String -> SymbolImport -> Module -> Either Error ImportChange
importModuleWithSymbol moduleName symbolImport module_
   | matching@(_:_) <- matchingImports moduleName (importDecls module_) =
      -- There is already a matching import line
      existingMatching matching moduleName symbolImport

   | not $ null (importDecls module_) =
      Right $ FindImportPos $ importDeclWithSymbol moduleName symbolImport

   | otherwise =
      case srcLineForNewImport module_ of
           Just srcLine -> Right $ AddImportAfter srcLine (importDeclWithSymbol moduleName symbolImport)
           Nothing      -> Right $ AddImportAtEnd (importDeclWithSymbol moduleName symbolImport)

-- | Extend the spec list with the given symbol.
-- Might result in duplciates.
extendSpecList :: SymbolImport -> HS.ImportSpecList Annotation -> HS.ImportSpecList Annotation
extendSpecList symbolImport (HS.ImportSpecList annotation hid specs) =
   HS.ImportSpecList annotation hid (specs ++ [importSpec $ symbol symbolImport])

-- | Remove an element from the import list if it matches the symbol.
-- If the resulting spec list is empty afterwards, Nothing is returned to remove
-- the import list.
-- Removes duplicate imports.
removeSpecList
   :: SymbolImport
   -> HS.ImportSpecList Annotation
   -> Either Error (Maybe (HS.ImportSpecList Annotation))
removeSpecList symbolImport (HS.ImportSpecList annotation hid specs) =
   let specListRemovedSymbol =
             traverse (removeSymbols (symbol symbolImport)) specs
   in  specListRemovedSymbol >>= \specList -> if null (catMaybes specList)
          then Right Nothing -- Remove the spec list if it is empty now
          else Right $ Just $ HS.ImportSpecList annotation
                                                hid
                                                (catMaybes specList)

 where
  removeSymbols :: Symbol -> ImportSpec -> Either Error (Maybe ImportSpec)
  removeSymbols (SomeOf symName _) t@(HS.IThingAll _ name) =
     if symName == nameString name
        then Left $ unlines
           [ "Tried to remove Constructors from a Type that exposed all constructors."
           , "This does not work because other Constructors are not available for HsImport."
           , "Thus, this operation can not be performed."
           ]
        else Right $ Just t

  removeSymbols (SomeOf symName names) t@(HS.IThingWith a hsSymName hsNames) =
     if symName == nameString hsSymName
        then Right $ Just (HS.IThingWith a hsSymName (removeFromList names hsNames))
        else Right $ Just t

  removeSymbols (AllOf symName) t@(HS.IThingWith a hsSymName _) =
      if symName == nameString hsSymName
         -- Remove all used constructors
         then Right $ Just (HS.IThingWith a hsSymName [])
         else Right $ Just t

  removeSymbols sym spec =
      if imports sym spec
         then Right $ Nothing
{'fraction_non_alphanumeric': 0.06200746578183326, 'fraction_numerical': 0.0, 'mean_word_length': 2.671827411167513, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3193489', 'n_tokens_mistral': 3612, 'n_tokens_neox': 3483, 'n_words': 1351}