LSP. Fix import errors location. Minor clean-up. Add tests for backend/diagnostics. (#868)

This commit is contained in:
PanAeon 2019-03-26 13:20:25 +00:00 committed by GitHub
parent 4c9dc60b20
commit e6be5c050e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 157 additions and 129 deletions

View File

@ -2,5 +2,6 @@
## unreleased ## unreleased
- whole document formatting - whole document formatting
- correctly show location of import errors
## 0.0.1.0 ## 0.0.1.0
- diagnostic output - diagnostic output

View File

@ -1,24 +1,9 @@
# dhall-lsp-server # dhall-lsp-server
```[![Travis](https://travis-ci.org/PanAeon/dhall-lsp-server.svg?branch=master)](https://travis-ci.org/PanAeon/dhall-lsp-server)```
**This project is in alpha state !!!**
This is a [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) server implementation for the [Dhall](https://dhall-lang.org) programming language. This is a [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) server implementation for the [Dhall](https://dhall-lang.org) programming language.
## Installation For installation or development instructions, see:
### From source
[Haskell Tool Stack](https://docs.haskellstack.org/en/stable/README/) should be installed.
```bash
cd ./dhall-lsp-server
stack install
```
Stack will copy executables to the current user's executable directory. On macOS this is `/Users/<username>/.local/bin`. On linux this should be `/Home/<username>/.local/bin`.
If you are using VSCode there's also an option in the [VSCode Dhall plugin](https://github.com/PanAeon/vscode-dhall-lsp-server) to specify the path to the executable directly, which might be useful if you have multiple executables or you can't use global PATH for some reason.
* [`dhall-haskell` - `README`](https://github.com/dhall-lang/dhall-haskell/blob/master/README.md)

View File

@ -93,9 +93,11 @@ executable dhall-lsp-server
test-suite dhall-lsp-server-test test-suite dhall-lsp-server-test
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
main-is: Spec.hs main-is: Driver.hs
other-modules: other-modules:
Paths_dhall_lsp_server Paths_dhall_lsp_server
Backend.Dhall.DiagnosticsSpec
hs-source-dirs: hs-source-dirs:
test test
default-extensions: LambdaCase OverloadedStrings FlexibleInstances TypeApplications RecordWildCards ScopedTypeVariables default-extensions: LambdaCase OverloadedStrings FlexibleInstances TypeApplications RecordWildCards ScopedTypeVariables

View File

@ -102,10 +102,8 @@ simpleTypeMessage (InvalidAlternativeType k expr0) =
simpleTypeMessage (InvalidAlternative k expr0) = simpleTypeMessage (InvalidAlternative k expr0) =
"Invalid alternative" "Invalid alternative"
simpleTypeMessage (AlternativeAnnotationMismatch k0 expr0 c0 k1 expr1 c1) =
-- ! FIXME: missing AlternativeAnnotationMismatch "Alternative annotation mismatch"
-- simpleTypeMessage (AlternativeAnnotationMismatch k0 expr0 c0 k1 expr1 c1) =
-- "Alternative annotation mismatch"
simpleTypeMessage (ListAppendMismatch expr0 expr1) = simpleTypeMessage (ListAppendMismatch expr0 expr1) =
"You can only append ❰List❱s with matching element types\n" "You can only append ❰List❱s with matching element types\n"

View File

@ -12,7 +12,7 @@ This module is responsible for producing dhall compiler diagnostic (errors, warn
import qualified Control.Exception import qualified Control.Exception
import qualified Dhall import qualified Dhall
import Dhall(rootDirectory, sourceName, defaultInputSettings, inputExprWithSettings) import Dhall(rootDirectory, sourceName, defaultInputSettings, inputExprWithSettings)
import Dhall.Parser(ParseError(..), Src(..)) import Dhall.Parser(ParseError(..), Src(..), SourcedException(..))
import qualified Dhall.Core import qualified Dhall.Core
import qualified System.Exit import qualified System.Exit
import qualified System.IO import qualified System.IO
@ -21,7 +21,7 @@ import Dhall.TypeCheck (DetailedTypeError(..), TypeError(..), X)
import Dhall.Binary(DecodingFailure(..)) import Dhall.Binary(DecodingFailure(..))
import Dhall.Import(Imported(..), Cycle(..), ReferentiallyOpaque(..), import Dhall.Import(Imported(..), Cycle(..), ReferentiallyOpaque(..),
MissingFile, MissingEnvironmentVariable, MissingImports ) MissingFile, MissingEnvironmentVariable, MissingImports(..) )
import qualified Data.Text as T import qualified Data.Text as T
@ -44,34 +44,27 @@ import Language.Haskell.LSP.Types(
, Position(..) , Position(..)
) )
defaultDiagnosticSource :: DiagnosticSource defaultDiagnosticSource :: DiagnosticSource
defaultDiagnosticSource = "dhall-lsp-server" defaultDiagnosticSource = "dhall-lsp-server"
-- FIXME: type errors span across whitespace after the expression -- TODO: type errors span across whitespace after the expression
-- Dhall.Binary.DecodingFailure -- TODO: don't use show for import msgs (requires alternative typeclass)
-- Dhall.Import(Cycle, ReferentiallyOpaque, MissingFile, MissingEnvironmentVariable, MissingImports, -- TODO: file consisting with only comments shouldn't produce an error msg
-- HashMismatch, CannotImportHTTPURL) compilerDiagnostics :: FilePath -> Text -> IO [Diagnostic]
-- !FIXME: (aside) VSCode multiselection expand selects first world only compilerDiagnostics path txt = handle ast
compilerDiagnostics :: FilePath -> Text -> Text -> IO [Diagnostic]
compilerDiagnostics path filePath txt = handle ast
where where
-- bufferName = T.unpack $ last $ fromList $ T.split (=='/') filePath
-- rootDir = T.unpack $ T.intercalate "/" $ tail $ fromList $ T.split (=='/') filePath
(rootDir, bufferName) = System.FilePath.splitFileName path (rootDir, bufferName) = System.FilePath.splitFileName path
settings = ( set rootDirectory rootDir settings = ( set rootDirectory rootDir
. set sourceName bufferName) defaultInputSettings . set sourceName bufferName) defaultInputSettings
isEmpty = T.null $ T.strip txt -- FIXME: file consisting with only comments shouldn't produce an error? handle upstream? isEmpty = T.null $ T.strip txt
ast = if isEmpty ast = if isEmpty
then pure [] then pure []
else [] <$ inputExprWithSettings settings txt else [] <$ inputExprWithSettings settings txt
handle = Control.Exception.handle allErrors handle = Control.Exception.handle allErrors
. Control.Exception.handle decodingFailure . Control.Exception.handle decodingFailure
. handleImportErrors txt . Control.Exception.handle missingImports
. Control.Exception.handle parseErrors . Control.Exception.handle parseErrors
. Control.Exception.handle importErrors . Control.Exception.handle typeErrors
. Control.Exception.handle moduleErrors
allErrors e = do allErrors e = do
@ -100,73 +93,52 @@ compilerDiagnostics path filePath txt = handle ast
errors = errorBundleToDiagnostics $ unwrap e errors = errorBundleToDiagnostics $ unwrap e
System.IO.hPrint System.IO.stderr errors System.IO.hPrint System.IO.stderr errors
pure $ errors pure $ errors
importErrors (Imported ps e) = do missingImports (SourcedException src e) = do
let _ = e :: TypeError Src X let _ = e :: MissingImports
numLines = length $ T.lines txt pure [Diagnostic {
System.IO.hPrint System.IO.stderr (show ps) _range = sourceToRange src
pure [ Diagnostic { , _severity = Just DsError
_range = Range (Position 0 0) (Position numLines 0) -- getSourceRange e , _source = Just defaultDiagnosticSource
, _severity = Just DsError , _code = Nothing
, _source = Just defaultDiagnosticSource , _message = removeAsciiColors $ show e
, _code = Nothing , _relatedInformation = Nothing
, _message = ("import error: " <> (show e)) -- FIXME: simple show for import msgs }]
, _relatedInformation = Nothing typeErrors e = do
}]
moduleErrors e = do
let _ = e :: TypeError Src X let _ = e :: TypeError Src X
(TypeError ctx expr msg) = e (TypeError ctx expr msg) = e
-- System.IO.hPrint System.IO.stderr txt
-- System.IO.hPrint System.IO.stderr e
pure [ Diagnostic { pure [ Diagnostic {
_range = getSourceRange e _range = getSourceRange e
, _severity = Just DsError , _severity = Just DsError
, _source = Just defaultDiagnosticSource , _source = Just defaultDiagnosticSource
, _code = Nothing , _code = Nothing
, _message = (simpleTypeMessage msg) -- FIXME: using show for import msgs , _message = (simpleTypeMessage msg)
, _relatedInformation = Nothing , _relatedInformation = Nothing
}] }]
-- ! FIXME: provide import errors source position (should be handled in the dhall project)
-- * Import Errors provide no source pos info, except import mode and ImportType (which contains actual url)
handleImportErrors :: Text -> IO [Diagnostic] -> IO [Diagnostic]
handleImportErrors txt = Control.Exception.handle (importHandler @Cycle)
. Control.Exception.handle (importHandler @ReferentiallyOpaque)
. Control.Exception.handle (importHandler @MissingFile)
. Control.Exception.handle (importHandler @MissingEnvironmentVariable)
. Control.Exception.handle (importHandler @MissingImports)
where
numLines = length $ T.lines txt
importHandler:: forall e a. Exception e => (e -> IO [Diagnostic])
importHandler e =
pure [Diagnostic {
_range = Range (Position 0 0) (Position numLines 0)
, _severity = Just DsError
, _source = Just defaultDiagnosticSource
, _code = Nothing
, _message = removeAsciiColors $ show e
, _relatedInformation = Nothing
}]
removeAsciiColors :: Text -> Text removeAsciiColors :: Text -> Text
removeAsciiColors = T.replace "\ESC[1;31m" "" . T.replace "\ESC[0m" "" removeAsciiColors = T.replace "\ESC[1;31m" "" . T.replace "\ESC[0m" ""
-- Dhall.Import(Cycle, ReferentiallyOpaque, MissingFile, MissingEnvironmentVariable, MissingImports,
-- HashMismatch, CannotImportHTTPURL)
getSourceRange :: TypeError Src X -> Range getSourceRange :: TypeError Src X -> Range
getSourceRange (TypeError ctx expr msg) = case expr of getSourceRange (TypeError ctx expr msg) = case expr of
Dhall.Core.Note (Src (Text.Megaparsec.SourcePos _ bl bc) (Text.Megaparsec.SourcePos _ el ec) _) _ -> Dhall.Core.Note src _ -> sourceToRange src
Range (Position (unPos bl - 1) (unPos bc - 1)) (Position (unPos el - 1) (unPos ec - 1)) _ -> error "Expected note" -- FIXME: either justify this error or provide a default case
_ -> error "expected note" -- $ Range (Position 0 0) (Position (negate 1) 0) -- FIXME: default case
where where
unPos = Text.Megaparsec.unPos unPos = Text.Megaparsec.unPos
sourcePosToRange :: Text.Megaparsec.SourcePos -> Text.Megaparsec.SourcePos -> Range
sourcePosToRange (Text.Megaparsec.SourcePos _ bl bc) (Text.Megaparsec.SourcePos _ el ec) =
Range (Position (unPos bl - 1) (unPos bc - 1)) (Position (unPos el - 1) (unPos ec - 1))
where
unPos = Text.Megaparsec.unPos
sourceToRange :: Src -> Range
sourceToRange (Src start end _) = sourcePosToRange start end
---------------------- Megaparsec utils: ---------------------------------------- ---------------------- Megaparsec utils: ----------------------------------------
-- see Text.Megaparsec.Error::errorBundlePretty for reference
errorBundleToDiagnostics errorBundleToDiagnostics
:: forall s e. ( Text.Megaparsec.Stream s :: forall s e. ( Text.Megaparsec.Stream s
, Text.Megaparsec.Error.ShowErrorComponent e , Text.Megaparsec.Error.ShowErrorComponent e
@ -216,45 +188,3 @@ errorFancyLength :: Text.Megaparsec.ShowErrorComponent e => Text.Megaparsec.Erro
errorFancyLength = \case errorFancyLength = \case
Text.Megaparsec.ErrorCustom a -> Text.Megaparsec.errorComponentLen a Text.Megaparsec.ErrorCustom a -> Text.Megaparsec.errorComponentLen a
_ -> 1 _ -> 1
-- errorBundlePretty
-- :: forall s e. ( Text.Megaparsec.Stream s
-- , Text.Megaparsec.Error.ShowErrorComponent e
-- )
-- => Text.Megaparsec.ParseErrorBundle s e -- ^ Parse error bundle to display
-- -> String -- ^ Textual rendition of the bundle
-- errorBundlePretty Text.Megaparsec.Error.ParseErrorBundle {..} =
-- let (r, _) = foldl' f (id, bundlePosState) bundleErrors
-- in drop 1 (r "")
-- where
-- f :: (ShowS, Text.Megaparsec.PosState s)
-- -> Text.Megaparsec.ParseError s e
-- -> (ShowS, Text.Megaparsec.PosState s)
-- f (o, !pst) e = (o . (outChunk ++), pst')
-- where
-- (epos, sline, pst') = reachOffset (errorOffset e) pst
-- outChunk =
-- "\n" <> sourcePosPretty epos <> ":\n" <>
-- padding <> "|\n" <>
-- lineNumber <> " | " <> sline <> "\n" <>
-- padding <> "| " <> rpadding <> pointer <> "\n" <>
-- Text.Megaparsec.Error.parseErrorTextPretty e
-- lineNumber = (show . unPos . sourceLine) epos
-- padding = replicate (length lineNumber + 1) ' '
-- rpadding =
-- if pointerLen > 0
-- then replicate rpshift ' '
-- else ""
-- rpshift = unPos (sourceColumn epos) - 1
-- pointer = replicate pointerLen '^'
-- pointerLen =
-- if rpshift + elen > slineLen
-- then slineLen - rpshift + 1
-- else elen
-- slineLen = length sline
-- elen =
-- case e of
-- Text.Megaparsec.TrivialError _ Nothing _ -> 1
-- Text.Megaparsec.TrivialError _ (Just x) _ -> errorItemLength x
-- Text.Megaparsec.FancyError _ xs ->
-- Data.Set.foldl' (\a b -> max a (errorFancyLength b)) 1 xs

View File

@ -42,7 +42,7 @@ sendDiagnostics fileUri version = do
let let
filePath = maybe (error "can't convert uri to file path") id $ J.uriToFilePath fileUri -- !FIXME: handle non-file uris filePath = maybe (error "can't convert uri to file path") id $ J.uriToFilePath fileUri -- !FIXME: handle non-file uris
txt <- lift $ Data.Text.IO.readFile filePath txt <- lift $ Data.Text.IO.readFile filePath
diags' <- lift $ compilerDiagnostics filePath (J.getUri fileUri) txt diags' <- lift $ compilerDiagnostics filePath txt
lift $ LSP.Utility.logs $ "diagnostic: " <> show diags' lift $ LSP.Utility.logs $ "diagnostic: " <> show diags'
publishDiagnostics 10 fileUri version (Map.union (partitionBySource diags') defaultDiagnosticBySource) publishDiagnostics 10 fileUri version (Map.union (partitionBySource diags') defaultDiagnosticBySource)

View File

@ -0,0 +1,113 @@
module Backend.Dhall.DiagnosticsSpec where
import Test.Tasty.Hspec
import Language.Haskell.LSP.Types(
Diagnostic(..)
, Range(..)
, DiagnosticSeverity(..)
, DiagnosticSource(..)
, DiagnosticRelatedInformation(..)
, Position(..)
)
import Backend.Dhall.Diagnostics
import qualified Data.Text
import qualified Data.Text.IO
spec_prelude :: Spec
spec_prelude = do
describe "Backend.Dhall.Diagnostics" $ do
it "should produce an empty diagnostics for an empty file" $ do
rs <- compilerDiagnostics "./empty.txt" ""
rs `shouldBe` ([])
it "should produce an empty diagnostics for a correct file (sanity check)" $ do
traverse_ (\x -> x `shouldReturn` []) (getDiagnostics <$> successImports)
it "should produce correct diagnostics for various missing imports" $ do
getDiagnostics "../dhall/dhall-lang/tests/import/failure/missing.dhall" `shouldReturn`
(mkDiagnostics "\nError: No valid imports" (0,0) (1,0))
getDiagnostics "../dhall/dhall-lang/tests/import/failure/alternativeEnv.dhall" `shouldReturn`
(mkDiagnostics ("\nError: Failed to resolve imports. Error list:\n\n\n\8627 env:UNSET1 as Text\n\n" <>
"Error: Missing environment variable\n\n\8627 UNSET1\n\n\n\8627 env:UNSET2\n\n" <>
"Error: Missing environment variable\n\n\8627 UNSET2\n\n\n\8627 env:UNSET3\n\n" <>
"Error: Missing environment variable\n\n\8627 UNSET3\n") (0,0) (1,0))
getDiagnostics "../dhall/dhall-lang/tests/import/failure/alternativeEnvMissing.dhall" `shouldReturn`
(mkDiagnostics "\n\8627 env:UNSET\n\nError: Missing environment variable\n\n\8627 UNSET" (0,0) (1,0))
getDiagnostics "../dhall/dhall-lang/tests/import/failure/cycle.dhall" `shouldReturn`
(mkDiagnostics ("\n\8627 ./../dhall/dhall-lang/tests/import/data/cycle.dhall\n" <>
" \8627 ./../dhall/dhall-lang/tests/import/failure/cycle.dhall\n\n" <>
"Cyclic import: ../data/cycle.dhall") (0,0) (1,0))
getDiagnostics "../dhall/dhall-lang/tests/import/failure/referentiallyInsane.dhall" `shouldReturn`
(mkDiagnostics ("\n\8627 https://raw.githubusercontent.com/dhall-lang/dhall-lang/master" <>
"/tests/import/data/referentiallyOpaque.dhall\n\n" <>
"Referentially opaque import: env:HOME as Text") (19,0) (20,0))
it "should produce correct diagnostic for various parser errors" $ do
getDiagnostics "../dhall/dhall-lang/tests/parser/failure/boundBuiltins.dhall" `shouldReturn`
(mkDiagnostics "expecting label or whitespace\n" (5,8) (5,9))
getDiagnostics "../dhall/dhall-lang/tests/parser/failure/doubleBoundsNeg.dhall" `shouldReturn`
(mkDiagnostics "double out of bounds\n" (0,0) (0,1))
getDiagnostics "../dhall/dhall-lang/tests/parser/failure/doubleBoundsPos.dhall" `shouldReturn`
(mkDiagnostics "double out of bounds\n" (0,0) (0,1))
getDiagnostics "../dhall/dhall-lang/tests/parser/failure/importAccess.dhall" `shouldReturn`
(mkDiagnostics "unexpected 'h'\nexpecting '/'\n" (0,14) (0,15))
getDiagnostics "../dhall/dhall-lang/tests/parser/failure/incompleteIf.dhall" `shouldReturn`
(mkDiagnostics "unexpected end of input\nexpecting expression or whitespace\n" (10,0) (10,1))
getDiagnostics "../dhall/dhall-lang/tests/parser/failure/mandatoryNewline.dhall" `shouldReturn`
(mkDiagnostics "unexpected \"AB\"\nexpecting crlf newline or newline\n" (1,2) (1,4))
getDiagnostics "../dhall/dhall-lang/tests/parser/failure/missingSpace.dhall" `shouldReturn`
(mkDiagnostics ("\n\8627 ./../dhall/dhall-lang/tests/parser/failure/example.dhall\n\n" <>
"Error: Missing file /Users/edevi86/lab/haskell/dhall-haskell/dhall-l" <>
"sp-server/../dhall/dhall-lang/tests/parser/failure/example.dhall") (1,0) (1,15))
it "should produce correct diagnostic for various typecheck errors" $ do
getDiagnostics "../dhall/dhall-lang/tests/typecheck/failure/combineMixedRecords.dhall" `shouldReturn`
(mkDiagnostics "Record mismatch" (0,0) (1,0))
it "should produce correct diagnostic for various typecheck errors" $ do
getDiagnostics "../dhall/dhall-lang/tests/typecheck/failure/duplicateFields.dhall" `shouldReturn`
(mkDiagnostics "duplicate field: a\n" (0,15) (0,16))
it "should produce correct diagnostic for various typecheck errors" $ do
getDiagnostics "../dhall/dhall-lang/tests/typecheck/failure/hurkensParadox.dhall" `shouldReturn`
(mkDiagnostics "\10096Sort\10097 has no type, kind, or sort" (6,4) (49,0))
it "should produce correct diagnostic for various typecheck errors" $ do
getDiagnostics "../dhall/dhall-lang/tests/typecheck/failure/mixedUnions.dhall" `shouldReturn`
(mkDiagnostics "Alternative annotation mismatch" (0,0) (1,0))
it "should produce correct diagnostic for various typecheck errors" $ do
getDiagnostics "../dhall/dhall-lang/tests/typecheck/failure/preferMixedRecords.dhall" `shouldReturn`
(mkDiagnostics "Record mismatch" (0,0) (1,0))
getDiagnostics :: FilePath -> IO [Diagnostic]
getDiagnostics path = do
text <- Data.Text.IO.readFile path
compilerDiagnostics path text
successImports = [ "../dhall/dhall-lang/tests/import/success/alternativeEnvNaturalA.dhall"
, "../dhall/dhall-lang/tests/import/success/alternativeEnvSimpleA.dhall"
, "../dhall/dhall-lang/tests/import/success/alternativeNaturalA.dhall"
, "../dhall/dhall-lang/tests/import/success/asTextA.dhall"
, "../dhall/dhall-lang/tests/import/success/fieldOrderA.dhall"
, "../dhall/dhall-lang/tests/import/success/alternativeEnvNaturalB.dhall"
, "../dhall/dhall-lang/tests/import/success/alternativeEnvSimpleB.dhall"
, "../dhall/dhall-lang/tests/import/success/alternativeNaturalB.dhall"
, "../dhall/dhall-lang/tests/import/success/asTextB.dhall"
, "../dhall/dhall-lang/tests/import/success/fieldOrderB.dhall"]
mkDiagnostics msg (sl, sc) (el, ec) = [
Diagnostic {
_range = Range {
_start = Position {_line = sl, _character = sc}
, _end = Position {_line = el, _character = ec}
}
, _severity = Just DsError
, _code = Nothing
, _source = Just "dhall-lsp-server"
, _message = msg
, _relatedInformation = Nothing
}]

View File

@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF tasty-discover #-}

View File

@ -1,2 +0,0 @@
main :: IO ()
main = putStrLn ("Test suite not yet implemented" :: String)