Skip to content

Commit

Permalink
Add quick-fix support for "Variable not in scope"
Browse files Browse the repository at this point in the history
  • Loading branch information
sol committed Jan 28, 2025
1 parent 87646c0 commit f77d82e
Show file tree
Hide file tree
Showing 19 changed files with 96 additions and 18 deletions.
37 changes: 35 additions & 2 deletions src/GHC/Diagnostic.hs
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
{-# LANGUAGE CPP #-}
module GHC.Diagnostic (
module Diagnostic
, Action(..)
, analyze
, apply
#ifdef TEST
, applyReplace
#endif
) where

import Prelude hiding (span)

import Imports

import System.IO
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Builder (hPutBuilder)

import GHC.Diagnostic.Type as Diagnostic

data Action = AddExtension FilePath Text
data Action = AddExtension FilePath Text | Replace Span Text
deriving (Eq, Show)

analyze :: Diagnostic -> Maybe Action
Expand All @@ -26,6 +32,8 @@ analyze diagnostic = listToMaybe $ mapMaybe analyzeHint diagnostic.hints
analyzeHint (T.pack -> hint) =
perhapsYouIntendedToUse
<|> enableAnyOfTheFollowingExtensions
<|> perhapsUse
<|> perhapsUseOneOfThese
where
perhapsYouIntendedToUse :: Maybe Action
perhapsYouIntendedToUse = do
Expand All @@ -37,10 +45,35 @@ analyze diagnostic = listToMaybe $ mapMaybe analyzeHint diagnostic.hints
T.stripPrefix "Enable any of the following extensions: " hint
>>= listToMaybe . reverse . map (AddExtension file) . T.splitOn ", "

perhapsUse :: Maybe Action
perhapsUse = mkPerhapsUse "Perhaps use `"

perhapsUseOneOfThese :: Maybe Action
perhapsUseOneOfThese = mkPerhapsUse "Perhaps use one of these:\n `"

mkPerhapsUse :: Text -> Maybe Action
mkPerhapsUse prefix = Replace <$> diagnostic.span <*> (takeIdentifier <$> T.stripPrefix prefix hint)
where
takeIdentifier :: Text -> Text
takeIdentifier = T.takeWhile (/= '\'')

apply :: Action -> IO ()
apply = \ case
AddExtension file name -> do
old <- B.readFile file
withFile file WriteMode $ \ h -> do
hPutBuilder h $ "{-# LANGUAGE " <> T.encodeUtf8Builder name <> " #-}\n"
B.hPutStr h old
Replace span substitute -> do
input <- B.readFile span.file <&> B.lines
B.writeFile span.file . B.unlines $
applyReplace span.start span.end substitute input

applyReplace :: Location -> Location -> Text -> [ByteString] -> [ByteString]
applyReplace start end substitute input = case splitAt (start.line - 1) input of
(xs, y : ys) | start.line == end.line -> xs ++ replaceInLine y : ys
_ -> input
where
replaceInLine :: ByteString -> ByteString
replaceInLine = T.decodeUtf8Lenient >>> T.splitAt (start.column - 1) >>> \ case
(xs, ys) -> T.encodeUtf8 $ xs <> substitute <> T.drop (end.column - start.column) ys
2 changes: 1 addition & 1 deletion src/HTTP.hs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ app getLastResult request respond = case pathInfo request of
getLastResult >>= \ case
(_, _, (analyze -> Just action) : _) -> apply action
_ -> pass
respond $ jsonResponse Status.ok200 "{}"
respond $ jsonResponse Status.noContent204 ""

_ -> do
respond $ genericStatus Status.notFound404 request
Expand Down
2 changes: 1 addition & 1 deletion src/Imports.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Data.Maybe as Imports
import Data.String as Imports
import Data.ByteString.Char8 as Imports (ByteString, pack, unpack)
import Data.Tuple as Imports
import System.FilePath as Imports hiding (combine)
import System.FilePath as Imports hiding (addExtension, combine)
import System.IO.Error as Imports (isDoesNotExistError)
import Text.Read as Imports (readMaybe)
import System.Exit as Imports (ExitCode(..))
Expand Down
41 changes: 33 additions & 8 deletions test/GHC/DiagnosticSpec.hs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
{-# LANGUAGE BlockArguments #-}
module GHC.DiagnosticSpec (spec) where

import Prelude hiding (span)

import Helper hiding (diagnostic)

import System.Process

import System.Environment
import Language.Haskell.GhciWrapper (lookupGhc)
import Data.Text (Text)

import Language.Haskell.GhciWrapper (lookupGhc)
import GHC.Diagnostic

test :: HasCallStack => FilePath -> Maybe Action -> Spec
test name edit = it name $ do
test name action = it name $ do
err <- translate <$> ghc ["-fno-diagnostics-show-caret"]
json <- encodeUtf8 <$> ghc ["-fdiagnostics-as-json", "--interactive", "-ignore-dot-ghci"]
ensureFile (dir </> "err.out") (encodeUtf8 err)
ensureFile (dir </> "err.json") json
Just diagnostic <- return $ parse json
decodeUtf8 (format diagnostic) `shouldBe` err
analyze diagnostic `shouldBe` edit
normalizeFileName <$> analyze diagnostic `shouldBe` action
where
dir :: FilePath
dir = "test" </> "assets" </> name
Expand All @@ -39,20 +41,43 @@ test name edit = it name $ do
'' -> '\''
c -> c

normalizeFileName :: Action -> Action
normalizeFileName = \ case
AddExtension _ name -> AddExtension "Foo.hs" name
Replace span substitute -> Replace span {file = "Foo.hs"} substitute

ftest :: HasCallStack => FilePath -> Maybe Action -> Spec
ftest name = focus . test name

_ignore :: HasCallStack => FilePath -> Maybe Action -> Spec
_ignore = ftest

replace :: Location -> Location -> Text -> Maybe Action
replace start end = Just . Replace (Span "Foo.hs" start end)

addExtension :: Text -> Maybe Action
addExtension = Just . AddExtension "Foo.hs"

spec :: Spec
spec = do
describe "format" $ do
test "variable-not-in-scope" Nothing
test "variable-not-in-scope-perhaps-use" Nothing
test "use-BlockArguments" (Just $ AddExtension "test/assets/use-BlockArguments/Foo.hs" "BlockArguments")
test "use-TemplateHaskellQuotes" (Just $ AddExtension "test/assets/use-TemplateHaskellQuotes/Foo.hs" "TemplateHaskellQuotes")
test "not-in-scope" Nothing
test "not-in-scope-perhaps-use" $ replace (Location 2 7) (Location 2 14) "filter"
test "not-in-scope-perhaps-use-one-of-these" $ replace (Location 2 7) (Location 2 11) "foldl"
test "not-in-scope-perhaps-use-multiline" $ replace (Location 3 7) (Location 3 11) "foldl"
test "use-BlockArguments" $ addExtension "BlockArguments"
test "use-TemplateHaskellQuotes" $ addExtension "TemplateHaskellQuotes"
test "non-existing" Nothing
test "parse-error" Nothing
test "lex-error" Nothing
test "multiple-error-messages" Nothing

describe "applyReplace" $ do
it "replaces a given source span with a substitute" $ do
applyReplace (Location 2 7) (Location 2 14) "filter" [
"module Foo where"
, "foo = filter_ p xs"
] `shouldBe` [
"module Foo where"
, "foo = filter p xs"
]
3 changes: 3 additions & 0 deletions test/assets/not-in-scope-perhaps-use-multiline/Foo.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module Foo where
import Data.List
foo = fold
1 change: 1 addition & 0 deletions test/assets/not-in-scope-perhaps-use-multiline/err.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"1.0","ghcVersion":"ghc-9.10.1","span":{"file":"test/assets/not-in-scope-perhaps-use-multiline/Foo.hs","start":{"line":3,"column":7},"end":{"line":3,"column":11}},"severity":"Error","code":88464,"message":["Variable not in scope: fold"],"hints":["Perhaps use one of these:\n `foldl' (imported from Data.List),\n `foldr' (imported from Data.List)"]}
7 changes: 7 additions & 0 deletions test/assets/not-in-scope-perhaps-use-multiline/err.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
test/assets/not-in-scope-perhaps-use-multiline/Foo.hs:3:7: error: [GHC-88464]
Variable not in scope: fold
Suggested fix:
Perhaps use one of these:
`foldl' (imported from Data.List),
`foldr' (imported from Data.List)

2 changes: 2 additions & 0 deletions test/assets/not-in-scope-perhaps-use-one-of-these/Foo.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module Foo where
foo = fold
1 change: 1 addition & 0 deletions test/assets/not-in-scope-perhaps-use-one-of-these/err.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"1.0","ghcVersion":"ghc-9.10.1","span":{"file":"test/assets/not-in-scope-perhaps-use-one-of-these/Foo.hs","start":{"line":2,"column":7},"end":{"line":2,"column":11}},"severity":"Error","code":88464,"message":["Variable not in scope: fold"],"hints":["Perhaps use one of these:\n `foldl' (imported from Prelude), `foldr' (imported from Prelude)"]}
6 changes: 6 additions & 0 deletions test/assets/not-in-scope-perhaps-use-one-of-these/err.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
test/assets/not-in-scope-perhaps-use-one-of-these/Foo.hs:2:7: error: [GHC-88464]
Variable not in scope: fold
Suggested fix:
Perhaps use one of these:
`foldl' (imported from Prelude), `foldr' (imported from Prelude)

File renamed without changes.
1 change: 1 addition & 0 deletions test/assets/not-in-scope-perhaps-use/err.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"1.0","ghcVersion":"ghc-9.10.1","span":{"file":"test/assets/not-in-scope-perhaps-use/Foo.hs","start":{"line":2,"column":7},"end":{"line":2,"column":14}},"severity":"Error","code":88464,"message":["Variable not in scope: filter_"],"hints":["Perhaps use `filter' (imported from Prelude)"]}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
test/assets/variable-not-in-scope-perhaps-use/Foo.hs:2:7: error: [GHC-88464]
test/assets/not-in-scope-perhaps-use/Foo.hs:2:7: error: [GHC-88464]
Variable not in scope: filter_
Suggested fix: Perhaps use `filter' (imported from Prelude)

File renamed without changes.
1 change: 1 addition & 0 deletions test/assets/not-in-scope/err.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":"1.0","ghcVersion":"ghc-9.10.1","span":{"file":"test/assets/not-in-scope/Foo.hs","start":{"line":2,"column":7},"end":{"line":2,"column":10}},"severity":"Error","code":88464,"message":["Variable not in scope: bar"],"hints":[]}
3 changes: 3 additions & 0 deletions test/assets/not-in-scope/err.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test/assets/not-in-scope/Foo.hs:2:7: error: [GHC-88464]
Variable not in scope: bar

1 change: 0 additions & 1 deletion test/assets/variable-not-in-scope-perhaps-use/err.json

This file was deleted.

1 change: 0 additions & 1 deletion test/assets/variable-not-in-scope/err.json

This file was deleted.

3 changes: 0 additions & 3 deletions test/assets/variable-not-in-scope/err.out

This file was deleted.

0 comments on commit f77d82e

Please sign in to comment.