aboutsummaryrefslogtreecommitdiff
path: root/src/HaskellCodeExplorer/Preprocessor.hs
blob: d99897670cdf5b31905eb262d95362d7a2cee816 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}

module HaskellCodeExplorer.Preprocessor
  ( createSourceCodeTransformation
  ) where

import Control.Applicative ((<|>))
import qualified Data.Attoparsec.Text as AT
import Data.Foldable (foldl')
import qualified Data.HashMap.Strict as HM
import qualified Data.List as L
import qualified Data.Set as S
import qualified Data.Text as T
import HaskellCodeExplorer.Types
  ( FileLocation(..)
  , HaskellFilePath(..)
  , HaskellModulePath(..)
  , LinePragma(..)
  , SourceCodeTransformation(..)
  , haskellPreprocessorExtensions
  )
import System.FilePath (normalise,takeExtension,takeFileName)

-- | Finds locations of line pragmas and creates an index
createSourceCodeTransformation ::
     HaskellModulePath -> T.Text -> T.Text -> (SourceCodeTransformation, T.Text)
createSourceCodeTransformation currentModulePath originalSourceCode sourceCodeAfterPreprocessing =
  let sourceCodeLines = T.splitOn "\n" sourceCodeAfterPreprocessing
      numberedLines = zip [1 :: Int ..] sourceCodeLines
      currentFilePath =
        HaskellFilePath . getHaskellModulePath $ currentModulePath
      addPragma :: [LinePragma] -> (Int, T.Text) -> [LinePragma]
      addPragma acc (lineNumber, line) =
        case AT.parseOnly linePragmaParser line of
          Right (originalLineNumber, mbFileName) ->
            LinePragma
              (maybe
                 currentFilePath
                 (HaskellFilePath . T.pack . normalise . T.unpack)
                 mbFileName)
              lineNumber
              originalLineNumber :
            acc
          Left _ -> acc
      totalLines = length numberedLines
      pragmas = L.reverse . L.foldl' addPragma [] $ numberedLines
      pragmaPath = filePath :: LinePragma -> HaskellFilePath
      currentFileExtension =
        takeExtension . T.unpack . getHaskellFilePath $ currentFilePath
      standardHeaderFiles =
        [ "stdc-predef.h"
        , "cabal_macros.h"
        , "ghcversion.h"
        , "HsVersions.h"
        , "ghc_boot_platform.h"
        , "ghcautoconf.h"
        ]
      hasIncludedFiles =
        L.any
          ((\path ->
              let fileName = takeFileName . T.unpack . getHaskellFilePath $ path
               in (path /= currentFilePath) &&
                  (path /= HaskellFilePath "<built-in>") &&
                  (path /= HaskellFilePath "<command-line>") &&
                  not ("ghc_" `L.isPrefixOf` fileName) &&
                  (fileName `notElem` standardHeaderFiles)) .
           pragmaPath)
          pragmas
   in if hasIncludedFiles ||
         currentFileExtension `elem` haskellPreprocessorExtensions
        then ( SourceCodeTransformation
                 totalLines
                 currentModulePath
                 (S.fromList pragmas)
                 (indexLocations totalLines currentFilePath pragmas)
             , sourceCodeAfterPreprocessing)
        else ( SourceCodeTransformation
                 (length $ T.splitOn "\n" originalSourceCode)
                 currentModulePath
                 S.empty
                 HM.empty
             , originalSourceCode)

-- | Parses line pragma
linePragmaParser :: AT.Parser (Int, Maybe T.Text)
linePragmaParser = pragma1 <|> pragma2
  where
    pragma1 :: AT.Parser (Int, Maybe T.Text)
    pragma1 = parser "#" "line"

    pragma2 :: AT.Parser (Int, Maybe T.Text)
    pragma2 = parser "{-#" "LINE"

    parser :: T.Text -> T.Text -> AT.Parser (Int, Maybe T.Text)
    parser start line = do
      _ <- AT.string start
      _ <- AT.takeWhile (== ' ')
      _ <- AT.string line <|> return ""
      _ <- AT.takeWhile (== ' ')
      num <- AT.decimal
      _ <- AT.takeWhile (== ' ')
      mbName <- (Just <$> fileName) <|> return Nothing
      return (num, mbName)

    fileName :: AT.Parser T.Text
    fileName = AT.string "\"" *> AT.takeTill (== '\"') <* AT.string "\""

data Line = FirstLine | LastLine Int | Pragma LinePragma deriving (Show,Eq)

-- | Creates a HashMap whose keys are filenames and values are locations in a
-- preprocessed source code
indexLocations ::
     Int
  -> HaskellFilePath
  -> [LinePragma]
  -> HM.HashMap HaskellFilePath (S.Set FileLocation)
indexLocations totalLines preprocessedFilePath pragmas =
  foldl' add HM.empty . (zip <*> tail) $
  (FirstLine : map Pragma pragmas) ++ [LastLine totalLines]
  where
    add ::
         HM.HashMap HaskellFilePath (S.Set FileLocation)
      -> (Line, Line)
      -> HM.HashMap HaskellFilePath (S.Set FileLocation)
    -- Interval between the first line and the first pragma
    add hMap (FirstLine, Pragma LinePragma {..})
      | lineNumberPreprocessed > 1 =
        HM.insertWith
          S.union
          preprocessedFilePath
          (S.singleton (FileLocation 1 lineNumberPreprocessed 0))
          hMap
      | otherwise = hMap
    -- Interval between two pragmas
    add hMap (Pragma (LinePragma fileName lineNumberPreprocessed1 lineNumberOriginal1),
              Pragma (LinePragma _ lineNumberPreprocessed2 _))
      | lineNumberPreprocessed2 - lineNumberPreprocessed1 > 1 =
        HM.insertWith
          S.union
          fileName
          (S.singleton
             (FileLocation
                lineNumberOriginal1
                (lineNumberOriginal1 +
                 (lineNumberPreprocessed2 - lineNumberPreprocessed1 - 2))
                (lineNumberPreprocessed1 - lineNumberOriginal1 + 1)))
          hMap
      | otherwise = hMap
    -- Interval between the last pragma and the last line
    add hMap (Pragma (LinePragma fileName lineNumberPreprocessed lineNumberOriginal),
              LastLine lastLineNumberPreprocessed)
      | lastLineNumberPreprocessed - lineNumberPreprocessed > 1 =
        HM.insertWith
          S.union
          fileName
          (S.singleton
             (FileLocation
                lineNumberOriginal
                (lineNumberOriginal + (lastLineNumberPreprocessed - lineNumberPreprocessed - 2))
                (lineNumberPreprocessed - lineNumberOriginal + 1)))
          hMap
      | otherwise = hMap
    add hMap _ = hMap