mirror of
https://github.com/IHaskell/IHaskell.git
synced 2025-04-15 19:06:06 +00:00
add hoogle module and hoogle test suite
This commit is contained in:
parent
15e989ef4d
commit
bc308f03da
@ -40,14 +40,17 @@ library
|
|||||||
megaparsec,
|
megaparsec,
|
||||||
semigroups,
|
semigroups,
|
||||||
haskell-src-exts,
|
haskell-src-exts,
|
||||||
|
http-client-tls,
|
||||||
|
http-client,
|
||||||
hlint,
|
hlint,
|
||||||
extra
|
extra
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
Jupyter.IHaskell.Complete
|
Jupyter.IHaskell.Complete
|
||||||
Jupyter.IHaskell.Evaluate
|
Jupyter.IHaskell.Evaluate
|
||||||
|
Jupyter.IHaskell.Hoogle
|
||||||
Jupyter.IHaskell.Interpreter
|
Jupyter.IHaskell.Interpreter
|
||||||
Jupyter.IHaskell.Parser
|
|
||||||
Jupyter.IHaskell.Lint
|
Jupyter.IHaskell.Lint
|
||||||
|
Jupyter.IHaskell.Parser
|
||||||
|
|
||||||
if flag(binPkgDb)
|
if flag(binPkgDb)
|
||||||
build-depends: bin-package-db
|
build-depends: bin-package-db
|
||||||
@ -72,8 +75,9 @@ Test-Suite test-ihaskell
|
|||||||
main-is: Test.hs
|
main-is: Test.hs
|
||||||
other-modules:
|
other-modules:
|
||||||
Jupyter.IHaskell.Test.Complete
|
Jupyter.IHaskell.Test.Complete
|
||||||
Jupyter.IHaskell.Test.Parser
|
Jupyter.IHaskell.Test.Hoogle
|
||||||
Jupyter.IHaskell.Test.Lint
|
Jupyter.IHaskell.Test.Lint
|
||||||
|
Jupyter.IHaskell.Test.Parser
|
||||||
hs-source-dirs: tests
|
hs-source-dirs: tests
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
build-depends:
|
build-depends:
|
||||||
|
236
src/Jupyter/IHaskell/Hoogle.hs
Normal file
236
src/Jupyter/IHaskell/Hoogle.hs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
module Jupyter.IHaskell.Hoogle (
|
||||||
|
hoogleSearch,
|
||||||
|
hoogleSearchExact,
|
||||||
|
HoogleResult(..),
|
||||||
|
renderHoogleResultHTML,
|
||||||
|
renderHoogleResultPlain,
|
||||||
|
) where
|
||||||
|
|
||||||
|
-- Imports from 'base'
|
||||||
|
import Control.Exception (catch, SomeException)
|
||||||
|
import Control.Monad (guard)
|
||||||
|
import Data.Bifunctor (second)
|
||||||
|
import Data.Char (isAscii, isAlphaNum)
|
||||||
|
import Data.Function ((&))
|
||||||
|
import Data.List (groupBy, elemIndex)
|
||||||
|
import Data.Monoid ((<>))
|
||||||
|
|
||||||
|
-- Imports from 'aeson'
|
||||||
|
import Data.Aeson (FromJSON(..), (.:), eitherDecode, Value(..))
|
||||||
|
|
||||||
|
-- Imports from 'text'
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
-- Imports from 'bytestring'
|
||||||
|
import Data.ByteString.Lazy (ByteString)
|
||||||
|
|
||||||
|
-- Imports from 'http-client'
|
||||||
|
import Network.HTTP.Client (withManager, parseUrl, httpLbs, responseBody, parseRequest)
|
||||||
|
|
||||||
|
-- Imports from 'http-client-tls'
|
||||||
|
import Network.HTTP.Client.TLS (tlsManagerSettings)
|
||||||
|
|
||||||
|
data HoogleResult =
|
||||||
|
HoogleResult
|
||||||
|
{ hoogleResultLocation :: Text
|
||||||
|
, hoogleResultSelf :: Text
|
||||||
|
, hoogleResultDocs :: Text
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
|
instance FromJSON HoogleResult where
|
||||||
|
parseJSON (Object obj) =
|
||||||
|
HoogleResult <$> obj .: "location" <*> obj .: "self" <*> obj .: "docs"
|
||||||
|
parseJSON _ = fail "Expected object with fields: location, self, docs"
|
||||||
|
|
||||||
|
newtype HoogleResults = HoogleResults { unHoogleResults :: [HoogleResult] }
|
||||||
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
|
instance FromJSON HoogleResults where
|
||||||
|
parseJSON (Object obj) = do
|
||||||
|
results <- obj .: "results"
|
||||||
|
HoogleResults <$> mapM parseJSON results
|
||||||
|
parseJSON _ = fail "Expected object with 'results' field."
|
||||||
|
|
||||||
|
-- | Look up an identifier on Hoogle. Return documentation for that identifier. If there are many
|
||||||
|
-- identifiers, include documentation for all of them.
|
||||||
|
hoogleSearchExact :: Text -> IO (Either String [HoogleResult])
|
||||||
|
hoogleSearchExact string = second filterHoogleMatches <$> hoogleSearch string
|
||||||
|
where
|
||||||
|
filterHoogleMatches :: [HoogleResult] -> [HoogleResult]
|
||||||
|
filterHoogleMatches = filter (isExactMatch string)
|
||||||
|
|
||||||
|
isExactMatch :: Text -> HoogleResult -> Bool
|
||||||
|
isExactMatch name HoogleResult { .. } = T.strip hoogleResultSelf == T.strip name
|
||||||
|
|
||||||
|
-- | Query Hoogle for the given string. This searches Hoogle using the internet. It returns either
|
||||||
|
-- an error message or the successful JSON result.
|
||||||
|
hoogleSearch :: Text -> IO (Either String [HoogleResult])
|
||||||
|
hoogleSearch str = do
|
||||||
|
let url = T.concat ["https://www.haskell.org/hoogle/?hoogle=", T.pack $ urlEncode $ T.unpack str, "&mode=json"]
|
||||||
|
flip catch handleException $ do
|
||||||
|
request <- parseRequest (T.unpack url)
|
||||||
|
parseResponse <$> withManager tlsManagerSettings (httpLbs request)
|
||||||
|
|
||||||
|
where
|
||||||
|
handleException :: SomeException -> IO (Either String a)
|
||||||
|
handleException exception = return . Left . show $ exception
|
||||||
|
|
||||||
|
parseResponse = second unHoogleResults . eitherDecode . responseBody
|
||||||
|
|
||||||
|
-- | Encode a string for use within a URL query parameter.
|
||||||
|
--
|
||||||
|
-- Copied directly from the HTTP package.
|
||||||
|
urlEncode :: String -> String
|
||||||
|
urlEncode [] = []
|
||||||
|
urlEncode (ch:t)
|
||||||
|
| (isAscii ch && isAlphaNum ch) || ch `elem` ("-_.~" :: String) = ch : urlEncode t
|
||||||
|
| not (isAscii ch) = foldr escape (urlEncode t) (eightBs [] (fromEnum ch))
|
||||||
|
| otherwise = escape (fromEnum ch) (urlEncode t)
|
||||||
|
where
|
||||||
|
escape :: Int -> String -> String
|
||||||
|
escape b rs = '%' : showH (b `div` 16) (showH (b `mod` 16) rs)
|
||||||
|
|
||||||
|
showH :: Int -> String -> String
|
||||||
|
showH x xs
|
||||||
|
| x <= 9 = toEnum (o_0 + x) : xs
|
||||||
|
| otherwise = toEnum (o_A + (x - 10)) : xs
|
||||||
|
where
|
||||||
|
o_0 = fromEnum '0'
|
||||||
|
o_A = fromEnum 'A'
|
||||||
|
|
||||||
|
eightBs :: [Int] -> Int -> [Int]
|
||||||
|
eightBs acc x
|
||||||
|
| x <= 255 = x : acc
|
||||||
|
| otherwise = eightBs ((x `mod` 256) : acc) (x `div` 256)
|
||||||
|
|
||||||
|
-- | Render a Hoogle result to plain text.
|
||||||
|
renderHoogleResultPlain :: HoogleResult -> Text
|
||||||
|
renderHoogleResultPlain HoogleResult { .. } =
|
||||||
|
T.concat [hoogleResultSelf, "\nURL: ", hoogleResultLocation, "\n", hoogleResultDocs]
|
||||||
|
|
||||||
|
-- | Render a Hoogle result to HTML.
|
||||||
|
renderHoogleResultHTML :: HoogleResult -> Text
|
||||||
|
renderHoogleResultHTML HoogleResult { .. } =
|
||||||
|
renderSelf hoogleResultSelf hoogleResultLocation <> renderDocs hoogleResultDocs
|
||||||
|
|
||||||
|
renderSelf :: Text -> Text -> Text
|
||||||
|
renderSelf string loc
|
||||||
|
| "package" `T.isPrefixOf` string =
|
||||||
|
T.concat [pkg, " ", mkSpan "hoogle-package" (mkLink loc $ extractPackage string)]
|
||||||
|
|
||||||
|
| "module" `T.isPrefixOf` string =
|
||||||
|
T.concat
|
||||||
|
[ mod
|
||||||
|
, " "
|
||||||
|
, mkSpan "hoogle-module" (mkLink loc $ extractModule string)
|
||||||
|
, packageSub (extractPackageName loc)
|
||||||
|
]
|
||||||
|
|
||||||
|
| "class" `T.isPrefixOf` string =
|
||||||
|
T.concat
|
||||||
|
[ cls
|
||||||
|
, " "
|
||||||
|
, mkSpan "hoogle-class" (mkLink loc $ extractClass string)
|
||||||
|
, packageSub (extractPackageName loc)
|
||||||
|
]
|
||||||
|
|
||||||
|
| "data" `T.isPrefixOf` string =
|
||||||
|
T.concat
|
||||||
|
[ dat
|
||||||
|
, " "
|
||||||
|
, mkSpan "hoogle-class" (mkLink loc $ extractData string)
|
||||||
|
, packageSub (extractPackageName loc)
|
||||||
|
]
|
||||||
|
|
||||||
|
| otherwise =
|
||||||
|
case T.splitOn "::" string of
|
||||||
|
[name, args] ->
|
||||||
|
mkSpan "hoogle-name"
|
||||||
|
(unicodeReplace $ mkLink loc (T.strip name) <> " :: " <> T.strip args)
|
||||||
|
<> packageAndModuleSub (extractPackageName loc) (extractModuleName loc)
|
||||||
|
where
|
||||||
|
extractPackage = T.strip . T.replace "package" ""
|
||||||
|
extractModule = T.strip . T.replace "module" ""
|
||||||
|
extractClass = T.strip . T.replace "class" ""
|
||||||
|
extractData = T.strip . T.replace "data" ""
|
||||||
|
pkg = mkSpan "hoogle-head" "package"
|
||||||
|
mod = mkSpan "hoogle-head" "module"
|
||||||
|
cls = mkSpan "hoogle-head" "class"
|
||||||
|
dat = mkSpan "hoogle-head" "data"
|
||||||
|
|
||||||
|
unicodeReplace :: Text -> Text
|
||||||
|
unicodeReplace =
|
||||||
|
T.replace "forall" "∀" .
|
||||||
|
T.replace "=>" "⇒" .
|
||||||
|
T.replace "->" "→" .
|
||||||
|
T.replace "::" "∷"
|
||||||
|
|
||||||
|
packageSub Nothing = ""
|
||||||
|
packageSub (Just package) =
|
||||||
|
mkSpan "hoogle-sub" $
|
||||||
|
T.concat ["(", pkg, " ", mkSpan "hoogle-package" package, ")"]
|
||||||
|
|
||||||
|
packageAndModuleSub Nothing _ = ""
|
||||||
|
packageAndModuleSub (Just package) Nothing = packageSub (Just package)
|
||||||
|
packageAndModuleSub (Just package) (Just modname) =
|
||||||
|
mkSpan "hoogle-sub" $
|
||||||
|
T.concat
|
||||||
|
[ "("
|
||||||
|
, pkg
|
||||||
|
, " "
|
||||||
|
, mkSpan "hoogle-package" package
|
||||||
|
, ", "
|
||||||
|
, mod
|
||||||
|
, " "
|
||||||
|
, mkSpan "hoogle-module" modname
|
||||||
|
, ")"
|
||||||
|
]
|
||||||
|
|
||||||
|
renderDocs :: Text -> Text
|
||||||
|
renderDocs docs =
|
||||||
|
docs &
|
||||||
|
T.lines &
|
||||||
|
groupBy bothAreCode &
|
||||||
|
map makeBlock &
|
||||||
|
T.unlines &
|
||||||
|
mkDiv "hoogle-doc"
|
||||||
|
where
|
||||||
|
bothAreCode x y = isCode [x] && isCode [y]
|
||||||
|
isCode ls =
|
||||||
|
case ls of
|
||||||
|
[] -> False
|
||||||
|
s:_ -> T.isPrefixOf ">" $ T.strip s
|
||||||
|
makeBlock ls =
|
||||||
|
mkDiv
|
||||||
|
(if isCode ls
|
||||||
|
then "hoogle-code"
|
||||||
|
else "hoogle-text")
|
||||||
|
(T.unlines $ filter (not . T.null . T.strip) ls)
|
||||||
|
|
||||||
|
extractPackageName :: Text -> Maybe Text
|
||||||
|
extractPackageName link = do
|
||||||
|
let pieces = T.splitOn "/" link
|
||||||
|
archiveLoc <- elemIndex "archive" pieces
|
||||||
|
latestLoc <- elemIndex "latest" pieces
|
||||||
|
guard $ latestLoc - archiveLoc == 2
|
||||||
|
return $ pieces !! (latestLoc - 1)
|
||||||
|
|
||||||
|
extractModuleName :: Text -> Maybe Text
|
||||||
|
extractModuleName link = do
|
||||||
|
case T.splitOn "/" link of
|
||||||
|
[] -> Nothing
|
||||||
|
pieces -> Just $ T.replace "-" "." $ T.takeWhile (/= '.') $ last pieces
|
||||||
|
|
||||||
|
mkDiv :: Text -> Text -> Text
|
||||||
|
mkDiv cls content = T.concat ["<div class='", cls, "'>", content, "</div>"]
|
||||||
|
|
||||||
|
mkSpan :: Text -> Text -> Text
|
||||||
|
mkSpan cls content = T.concat ["<span class='", cls, "'>", content, "</span>"]
|
||||||
|
|
||||||
|
mkLink :: Text -> Text -> Text
|
||||||
|
mkLink href content = T.concat ["<a target='_blank' href='", href, "'>", content, "</a>"]
|
94
tests/Jupyter/IHaskell/Test/Hoogle.hs
Normal file
94
tests/Jupyter/IHaskell/Test/Hoogle.hs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{-|
|
||||||
|
Module : Jupyter.IHaskell.Test.Hoogle
|
||||||
|
Description : Tests for Jupyter.IHaskell.Hoogle
|
||||||
|
Copyright : (c) Andrew Gibiansky, 2016
|
||||||
|
License : MIT
|
||||||
|
Maintainer : andrew.gibiansky@gmail.com
|
||||||
|
Stability : stable
|
||||||
|
Portability : POSIX
|
||||||
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
module Jupyter.IHaskell.Test.Hoogle (hoogleTests) where
|
||||||
|
|
||||||
|
-- Imports from 'base'
|
||||||
|
import Control.Monad (unless)
|
||||||
|
|
||||||
|
-- Imports from 'text'
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
-- Imports from 'tasty'
|
||||||
|
import Test.Tasty (TestTree)
|
||||||
|
|
||||||
|
-- Imports from 'tasty-hunit'
|
||||||
|
import Test.Tasty.HUnit (testCase, assertFailure)
|
||||||
|
|
||||||
|
-- Imports from 'transformers'
|
||||||
|
import Control.Monad.IO.Class (MonadIO(..))
|
||||||
|
|
||||||
|
-- Imports from 'ihaskell'
|
||||||
|
import Jupyter.IHaskell.Hoogle (hoogleSearch, hoogleSearchExact, HoogleResult(..))
|
||||||
|
|
||||||
|
-- | Type of Hoogle query to issue.
|
||||||
|
data Query = ExactMatch Text -- ^ Search for a string
|
||||||
|
| InexactMatch Text -- ^ Search for a precise identifier match
|
||||||
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
|
-- | Test the IHaskell HLint integration.
|
||||||
|
hoogleTests :: TestTree
|
||||||
|
hoogleTests = testCase "Hoogle" $ do
|
||||||
|
-- Exact matches.
|
||||||
|
ExactMatch "map" --> [ hoogle "base" "Prelude.html#v:map" "map :: (a -> b) -> [a] -> [b]"
|
||||||
|
"map f xs is the list obtained by applying f to each element"
|
||||||
|
]
|
||||||
|
ExactMatch "fmap" --> [HoogleResult "" "" ""]
|
||||||
|
ExactMatch "Data.Text" --> [HoogleResult "" "" ""]
|
||||||
|
ExactMatch "base" --> [HoogleResult "" "" ""]
|
||||||
|
ExactMatch "FromJSON" --> [HoogleResult "" "" ""]
|
||||||
|
ExactMatch "Monad" --> [HoogleResult "" "" ""]
|
||||||
|
|
||||||
|
-- Inexact matches.
|
||||||
|
InexactMatch "map" --> [HoogleResult "" "" ""]
|
||||||
|
InexactMatch "fmap" --> [HoogleResult "" "" ""]
|
||||||
|
InexactMatch "Data.Text" --> [HoogleResult "" "" ""]
|
||||||
|
InexactMatch "base" --> [HoogleResult "" "" ""]
|
||||||
|
InexactMatch "Monad" --> [HoogleResult "" "" ""]
|
||||||
|
|
||||||
|
-- Type signature searches.
|
||||||
|
InexactMatch ":: a -> a" --> [HoogleResult "" "" ""]
|
||||||
|
InexactMatch ":: Monoid w => w -> w -> w" --> [HoogleResult "" "" ""]
|
||||||
|
|
||||||
|
hoogle :: Text -> Text -> Text -> Text -> HoogleResult
|
||||||
|
hoogle pkg url =
|
||||||
|
HoogleResult
|
||||||
|
(T.concat ["http://hackage.haskell.org/packages/archive/", pkg, "/latest/doc/html/", url])
|
||||||
|
|
||||||
|
|
||||||
|
-- | Run a single linter test. Lint the input and then compare output suggestions to generated
|
||||||
|
-- suggestions.
|
||||||
|
(-->) :: Query -> [HoogleResult] -> IO ()
|
||||||
|
query --> expected = do
|
||||||
|
result <- case query of
|
||||||
|
ExactMatch txt -> hoogleSearch txt
|
||||||
|
InexactMatch txt -> hoogleSearchExact txt
|
||||||
|
case result of
|
||||||
|
Left err -> assertFailure $ "Hoogle Error: " ++ err
|
||||||
|
Right observed ->
|
||||||
|
unless (and (zipWith sameResult expected observed) && length expected <= length observed) $
|
||||||
|
assertFailure $
|
||||||
|
concat
|
||||||
|
[ "Did not get expected Hoogle results for: "
|
||||||
|
, show query
|
||||||
|
, "\nExpected: "
|
||||||
|
, show expected
|
||||||
|
, "\nObserved: "
|
||||||
|
, show observed
|
||||||
|
]
|
||||||
|
|
||||||
|
where
|
||||||
|
sameResult (HoogleResult url1 self1 doc1) (HoogleResult url2 self2 doc2) =
|
||||||
|
url1 == url2 &&
|
||||||
|
self1 == self2 &&
|
||||||
|
and (zipWith (==) (T.unpack doc1) (T.unpack doc2)) &&
|
||||||
|
T.length doc1 <= T.length doc2
|
@ -89,7 +89,7 @@ lintTests = testCase "Linting" $ do
|
|||||||
block --> expected = do
|
block --> expected = do
|
||||||
observed <- lintCodeBlock (Loc 1 block)
|
observed <- lintCodeBlock (Loc 1 block)
|
||||||
unless (and (zipWith (==) expected observed) && length expected == length observed) $
|
unless (and (zipWith (==) expected observed) && length expected == length observed) $
|
||||||
liftIO $ assertFailure $
|
assertFailure $
|
||||||
concat
|
concat
|
||||||
[ "Did not get expected suggestions for: "
|
[ "Did not get expected suggestions for: "
|
||||||
, show block
|
, show block
|
||||||
|
@ -5,6 +5,7 @@ import Test.Tasty (defaultMain, testGroup)
|
|||||||
|
|
||||||
-- Imports from 'ihaskell'
|
-- Imports from 'ihaskell'
|
||||||
import Jupyter.IHaskell.Test.Complete (completionTests)
|
import Jupyter.IHaskell.Test.Complete (completionTests)
|
||||||
|
import Jupyter.IHaskell.Test.Hoogle (hoogleTests)
|
||||||
import Jupyter.IHaskell.Test.Lint (lintTests)
|
import Jupyter.IHaskell.Test.Lint (lintTests)
|
||||||
import Jupyter.IHaskell.Test.Parser (parserTests)
|
import Jupyter.IHaskell.Test.Parser (parserTests)
|
||||||
|
|
||||||
@ -12,4 +13,4 @@ import Jupyter.IHaskell.Test.Parser (parserTests)
|
|||||||
main :: IO ()
|
main :: IO ()
|
||||||
main =
|
main =
|
||||||
defaultMain $
|
defaultMain $
|
||||||
testGroup "Tests" [parserTests, completionTests, lintTests]
|
testGroup "Tests" [parserTests, completionTests, lintTests, hoogleTests]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user