Skip to content

Filter generated Core variable names from hovertip documentation #3316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 7, 2023

Conversation

drone29a
Copy link
Contributor

@drone29a drone29a commented Nov 2, 2022

This addresses #3280 by checking if names associated with a Haskell source file location follow particular prefix patterns used by generated Core variables. The two prefixes used are: $d and $c. If a name is prefixed with either of those strings then it is removed from the list of names used to generate the hovertip documentation content.

This change has been tested with the cases mentioned in the associated issue.

@santiweight
Copy link
Collaborator

If it's not too inconvenient: is it possible to have a test for this? Something with Generics is a great candidate, because those are the most problematic in my experience

Copy link
Collaborator

@fendor fendor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good start, would it be possible to add the two cases you mention in the associated issues as test-cases? Otherwise, regressions or changes in how the internal names look like will go through unchecked.

Comment on lines 228 to 232
isInternal :: (Identifier, IdentifierDetails a) -> Bool
isInternal (Right n, _) =
let name = printOutputable n
prefix = T.take 2 name
in elem prefix ["$d", "$c"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding a comment, what we consider to be an internal name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. I had done this locally right before pushing, but forgot to commit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any logic like this anywhere else in the code? Is there a more reliable way to identify a generated name? Have you asked in #ghc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, there is a list of GHC OccName prefixes in Development.IDE.Plugin.Completions.Logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: tests, is there a good example test or test module I can use as a reference? It looks like there aren't existing tests for the AtPoint module. Should ghcide tests be placed within the top-level test/ directory or the ghcide/test/ directory?

I think we'll want to move the isInternal function into the Development.IDE.Plugin.Completions.Logic module, and perhaps give it a slightly better name. We could also extract the retrieval of names from the atPoint function, make it a top-level function (getNamesAtPoint?), and then test that directly rather than test the result of atPoint.

I'm new to HLS dev, so I'll proceed with whatever the experienced members recommend.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @drone-rites sorry for the late response. The ghcide test suite lives all in ghcide/test/exe/Main.hs and is almost 100% composed of integration tests, you will find almost no unit tests at all. The best place to test this change is:

findDefinitionAndHoverTests :: TestTree
findDefinitionAndHoverTests = let
tst :: (TextDocumentIdentifier -> Position -> Session a, a -> Session [Expect] -> Session ()) -> Position -> String -> Session [Expect] -> String -> TestTree
tst (get, check) pos sfp targetRange title = testSessionWithExtraFiles "hover" title $ \dir -> do
-- Dirty the cache to check that definitions work even in the presence of iface files
liftIO $ runInDir dir $ do
let fooPath = dir </> "Foo.hs"
fooSource <- liftIO $ readFileUtf8 fooPath
fooDoc <- createDoc fooPath "haskell" fooSource
_ <- getHover fooDoc $ Position 4 3
closeDoc fooDoc
doc <- openTestDataDoc (dir </> sfp)
waitForProgressDone
found <- get doc pos
check found targetRange
checkHover :: Maybe Hover -> Session [Expect] -> Session ()
checkHover hover expectations = traverse_ check =<< expectations where
check expected =
case hover of
Nothing -> unless (expected == ExpectNoHover) $ liftIO $ assertFailure "no hover found"
Just Hover{_contents = (HoverContents MarkupContent{_value = standardizeQuotes -> msg})
,_range = rangeInHover } ->
case expected of
ExpectRange expectedRange -> checkHoverRange expectedRange rangeInHover msg
ExpectHoverRange expectedRange -> checkHoverRange expectedRange rangeInHover msg
ExpectHoverText snippets -> liftIO $ traverse_ (`assertFoundIn` msg) snippets
ExpectHoverTextRegex re -> liftIO $ assertBool ("Regex not found in " <> T.unpack msg) (msg =~ re :: Bool)
ExpectNoHover -> liftIO $ assertFailure $ "Expected no hover but got " <> show hover
_ -> pure () -- all other expectations not relevant to hover
_ -> liftIO $ assertFailure $ "test not expecting this kind of hover info" <> show hover
extractLineColFromHoverMsg :: T.Text -> [T.Text]
extractLineColFromHoverMsg = T.splitOn ":" . head . T.splitOn "*" . last . T.splitOn (sourceFileName <> ":")
checkHoverRange :: Range -> Maybe Range -> T.Text -> Session ()
checkHoverRange expectedRange rangeInHover msg =
let
lineCol = extractLineColFromHoverMsg msg
-- looks like hovers use 1-based numbering while definitions use 0-based
-- turns out that they are stored 1-based in RealSrcLoc by GHC itself.
adjust Position{_line = l, _character = c} =
Position{_line = l + 1, _character = c + 1}
in
case map (read . T.unpack) lineCol of
[l,c] -> liftIO $ adjust (_start expectedRange) @=? Position l c
_ -> liftIO $ assertFailure $
"expected: " <> show ("[...]" <> sourceFileName <> ":<LINE>:<COL>**[...]", Just expectedRange) <>
"\n but got: " <> show (msg, rangeInHover)
assertFoundIn :: T.Text -> T.Text -> Assertion
assertFoundIn part whole = assertBool
(T.unpack $ "failed to find: `" <> part <> "` in hover message:\n" <> whole)
(part `T.isInfixOf` whole)
sourceFilePath = T.unpack sourceFileName
sourceFileName = "GotoHover.hs"
mkFindTests tests = testGroup "get"
[ testGroup "definition" $ mapMaybe fst tests
, testGroup "hover" $ mapMaybe snd tests
, checkFileCompiles sourceFilePath $
expectDiagnostics
[ ( "GotoHover.hs", [(DsError, (62, 7), "Found hole: _")])
, ( "GotoHover.hs", [(DsError, (65, 8), "Found hole: _")])
]
, testGroup "type-definition" typeDefinitionTests
, testGroup "hover-record-dot-syntax" recordDotSyntaxTests ]
typeDefinitionTests = [ tst (getTypeDefinitions, checkDefs) aaaL14 sourceFilePath (pure tcData) "Saturated data con"
, tst (getTypeDefinitions, checkDefs) aL20 sourceFilePath (pure [ExpectNoDefinitions]) "Polymorphic variable"]
recordDotSyntaxTests
| ghcVersion >= GHC92 =
[ tst (getHover, checkHover) (Position 19 24) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["x :: MyRecord"]]) "hover over parent"
, tst (getHover, checkHover) (Position 19 25) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over dot shows child"
, tst (getHover, checkHover) (Position 19 26) (T.unpack "RecordDotSyntax.hs") (pure [ExpectHoverText ["_ :: MyChild"]]) "hover over child"
]
| otherwise = []
test runDef runHover look expect = testM runDef runHover look (return expect)
testM runDef runHover look expect title =
( runDef $ tst def look sourceFilePath expect title
, runHover $ tst hover look sourceFilePath expect title ) where
def = (getDefinitions, checkDefs)
hover = (getHover , checkHover)
-- search locations expectations on results

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @pepeiborra, thanks for the pointer; perfect timing as I was just refactoring the updates today.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a single, minimal test that captures the case I was originally observing. Type class dictionary variables associated with a type definition were showing up in the hover info when cursor was placed on a comment above the type definition.

@drone29a drone29a force-pushed the filter-atpoint-results branch from ece5bb9 to 764f716 Compare November 21, 2022 00:12
@drone29a
Copy link
Contributor Author

It's worth noting that reusing the occNamePrefixes list from:
https://github.com/drone-rites/haskell-language-server/blob/764f716e5390bdf9cb0fc602823171e8296285cb/ghcide/src/Development/IDE/GHC/CoreFile.hs#L234-L274
contain some entries that are redundant for the purpose of checking if a name is Core-generated. For example, the entries $contag and $co are redundant given the presence of $c. The additional longer prefixes are only needed for correctly stripping prefixes. I decided to keep it simple in the current version of the PR and there will be some redundant checks.

Copy link
Collaborator

@pepeiborra pepeiborra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, thank you for the test. I only have one question: can someone remind me why aren't we just filtering all identifiers starting with $, other than $ itself?

Copy link
Collaborator

@fendor fendor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, amazing and necessary improvement :)

@drone29a
Copy link
Contributor Author

This looks good to me, thank you for the test. I only have one question: can someone remind me why aren't we just filtering all identifiers starting with $, other than $ itself?

I started with a predicate like that locally, but then moved to an explicit short list of $d and $c after recalling there are other valid Haskell operators that match—e.g., $!, $$, and $-$. That short list was expanded once we discovered there was a list of Core variable prefixes already in ghcide.

An alternative predicate that should work is: the identifier has length of at least two characters, starts with "$", and contains an alphanumeric. Maybe there is a specification that describes the naming scheme of generated Core symbols and we could later update this check to follow it.

@michaelpj
Copy link
Collaborator

It seems that SrcSpans can be UnhelpfulSpans (https://hackage.haskell.org/package/ghc-9.4.2/docs/GHC-Types-SrcLoc.html#t:SrcSpan) which can indicate that they are generated. There's even a function for it: https://hackage.haskell.org/package/ghc-9.4.2/docs/GHC-Types-SrcLoc.html#v:isGeneratedSrcSpan

Could we replace the name-based logic with some logic that instead uses the SrcSpan of the Name?

@drone29a
Copy link
Contributor Author

It seems that SrcSpans can be UnhelpfulSpans (https://hackage.haskell.org/package/ghc-9.4.2/docs/GHC-Types-SrcLoc.html#t:SrcSpan) which can indicate that they are generated. There's even a function for it: https://hackage.haskell.org/package/ghc-9.4.2/docs/GHC-Types-SrcLoc.html#v:isGeneratedSrcSpan

Could we replace the name-based logic with some logic that instead uses the SrcSpan of the Name?

Unfortunately, it doesn't look like access to UnhelpfulSpans are provided through an HieAST a. The nodeSpan field in an HieAST a is a Span type which is just an alias for RealSrcSpan. That said, the generated variables are showing up when the point is on a non-whitespace character of a comment, which seems like it would be covered by a RealSrcSpan. We could dive deeper and figure out why generated variables are being linked with comment spans.

@drone29a drone29a force-pushed the filter-atpoint-results branch from aa61fcc to 66b2614 Compare November 28, 2022 19:05
@michaelpj
Copy link
Collaborator

That does seem odd indeed. A question for @wz1000 I guess.

@michaelpj
Copy link
Collaborator

Sorry, I thought this was merged! I think this was basically good to go, although I would appreciate it if @wz1000 could take a look and think about the issue with detecting generated code.

@wz1000
Copy link
Collaborator

wz1000 commented Jan 25, 2023

Sorry, I didn't look at this earlier, but I'm not sure if looking at occname prefixes is the best way to go about this.

I think this line from #1983 might do what you need to out evidence bindings ($dFoo and $fFoo:

        prettyName (Right n, dets)
#if MIN_VERSION_ghc(9,0,1)
          | any isEvidenceUse (identInfo dets) = maybe "" (printOutputable . renderEvidenceTree) (getEvidenceTree _rf n) <> "\n"

https://github.com/haskell/haskell-language-server/pull/1983/files#diff-91f773bae93bbf86035573f4dbffa3035c1689b1bb7e17bde5937bb1271c0131R251

I'm not completely confident that it will remove all the generated names, but you could also have better luck filtering them out if you dropped the generated code like so:

removeGenerated :: HieAstResult -> HieAstResult
removeGenerated HAR{..} = HAR{hieAst = go hieAst,..}
where
go :: HieASTs a -> HieASTs a
go hf =
#if MIN_VERSION_ghc(9,2,1)
HieASTs (fmap goAst (getAsts hf))
goAst (Node nsi sp xs) = Node (SourcedNodeInfo $ M.restrictKeys (getSourcedNodeInfo nsi) (S.singleton SourceInfo)) sp (map goAst xs)
#else
hf
#endif

Maybe these two fixes won't be enough and we would still need to filter out some names based on the prefix (like $sel in the presence of Duplicated record fields), so I don't know the best way forward.

@michaelpj
Copy link
Collaborator

@wz1000 what do you think about the idea of preserving the UnhelpfulGenerated SrcSpans through HIE files so we can pick them up again later?

@drone29a
Copy link
Contributor Author

@wz1000 thanks, I was hoping there'd be a more direct approach. I will try out your suggestions.

@drone29a
Copy link
Contributor Author

drone29a commented Feb 3, 2023

I tested updating prettyLine to use isEvidenceUse like so:

        prettyName (Right n, dets)
#if MIN_VERSION_ghc(9,0,1)
          | any isEvidenceUse (identInfo dets) = "wat...\n"
          | otherwise
#endif
           = T.unlines $
          wrapHaskell (printOutputable n <> maybe "" (" :: " <>) ((prettyType <$> identType dets) <|> maybeKind))
          : maybeToList (pretty (definedAt n) (prettyPackageName n))
          ++ catMaybes [ T.unlines . spanDocToMarkdown <$> lookupNameEnv dm n
                       ]
          where maybeKind = fmap printOutputable $ safeTyThingType =<< lookupNameEnv km n
                pretty Nothing Nothing = Nothing
                pretty (Just define) Nothing = Just $ define <> "\n"
                pretty Nothing (Just pkgName) = Just $ pkgName <> "\n"
                pretty (Just define) (Just pkgName) = Just $ define <> " " <> pkgName <> "\n"

It failed to replace the entries for the evidence bindings with the test string "wat...\n".

I also tried using removeGenerated which I modified to operate directly on HieASTs a and used like so:

atPoint IdeOptions{} (HAR _ hf _ _ kind) (DKMap dm km) env pos = listToMaybe $ pointCommand (removeGenerated hf) pos hoverInfo

It also failed to remove the entries.

@wz1000 any other suggestions I should try? Or did I misunderstand your earlier suggestions? Thanks!

@wz1000
Copy link
Collaborator

wz1000 commented Feb 3, 2023

Ah, try isEvidenceContext which should catch more cases.

… CoreFile module. Updated the check for GHC-generated OccName prefixes in AtPoint to use the shared list of OccName prefixes.
… CoreFile module. Updated the check for GHC-generated OccName prefixes in AtPoint to use the shared list of OccName prefixes.
… CoreFile module. Updated the check for GHC-generated OccName prefixes in AtPoint to use the shared list of OccName prefixes.
@drone29a drone29a force-pushed the filter-atpoint-results branch from 7b83233 to bbd73da Compare February 3, 2023 22:59
@drone29a
Copy link
Contributor Author

drone29a commented Feb 3, 2023

That works, thanks! Sorry for the noise on the PR, somewhere along the lines I merged master into the branch instead of rebasing.

Copy link
Collaborator

@michaelpj michaelpj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lovely!

@michaelpj
Copy link
Collaborator

It doesn't seem to build however, it complains that isEvidenceContext is not in scope?


-- | Prefixes that can occur in a GHC OccName
occNamePrefixes :: [T.Text]
occNamePrefixes =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed now?

Copy link
Contributor Author

@drone29a drone29a Feb 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a refactor to allow the prefixes from the Logic module available to other plugins. Thanks to your suggestion, we now don't need to access to those prefixes from the AtPoint module but other modules might need access to the prefixes in the future. We can revert the refactor, or if it's considered an improvement we can keep it. I'll defer to you and others with more experience on the codebase.

@drone29a
Copy link
Contributor Author

drone29a commented Feb 4, 2023

@michaelpj there was a missing preprocessor check. This commit should fix that: 30c42bb

@michaelpj michaelpj merged commit b8205fa into haskell:master Feb 7, 2023
@michaelpj
Copy link
Collaborator

Thanks!

wz1000 pushed a commit that referenced this pull request Mar 14, 2023
* Filter names from hovertip documentation which match patterns of generated Core variables.

* Added comment for isInternal function.

* Relocated list of OccName prefixes from the Completions plugin to the CoreFile module. Updated the check for GHC-generated OccName prefixes in AtPoint to use the shared list of OccName prefixes.

* Added test data and test for ensuring Core-generated variables are not included in hover info.

* Switched to using isEvidenceContext to filter out evidence bindings.

* Relocated list of OccName prefixes from the Completions plugin to the CoreFile module. Updated the check for GHC-generated OccName prefixes in AtPoint to use the shared list of OccName prefixes.

* Removed unused import.

* Relocated list of OccName prefixes from the Completions plugin to the CoreFile module. Updated the check for GHC-generated OccName prefixes in AtPoint to use the shared list of OccName prefixes.

* Removed unused import.

* Fixed incorrect definition of mkRecordSnippetCompItem introduced when rebasing.

* Add preprocessor check for GHC version before using isEvidenceContext. Updated related comment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants