haskell / haskell-language-server

Official haskell ide support via language server (LSP). Successor of ghcide & haskell-ide-engine.
Apache License 2.0
2.71k stars 368 forks source link

Better reference finding on record fields #3437

Open jasagredo opened 1 year ago

jasagredo commented 1 year ago

Is your enhancement request related to a problem? Please describe.

When exploring a codebase it is often useful to see where a field is being used either accessed or set. What I usually do is manually grep for the field name or ask HLS to find the references for me. However these two methods have their downsides, in particular none of them find the places where the record field is accessed via pattern matching.

Describe the solution you'd like

I think it would be incredibly useful if requesting for the references to a field also returned the places where the field is bound by pattern matching.

Describe alternatives you've considered

Alternative solutions/hacks I have used or have heard of:

  1. Grep for the name of the field. Problem: doesn't find pattern matches.
  2. Ask HLS for references. Problem: doesn't find pattern matches.
  3. Change the type of the field so that one gets type errors in the usage places. Problem: hacky? and an error in one file might prevent later files of being checked, thus hiding other usages and requiring work to find them
  4. Always pattern match with NamedFieldPuns. Problem: requires discipline and might need a big syntactic rework on existing codebases.

Additional context

I created the following file and asked HLS to return references to the field field. I marked the found ones, however it would be awesome if every one of those usages was found by HLS:

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}

...

data Foo = Foo { field :: Int }

-- textDocument/references on `field` above

foo :: Foo -> Int
foo (Foo f) = f
foo (Foo field) = field
foo Foo { {- found -} field } = field
foo Foo { {- found -} field = f } = f
foo Foo{ {- found -} ..} = field
foo f = ff
  where Foo ff = f
foo f = field
  where Foo field = f
foo f = field
  where Foo { {- found -} field} = f
foo f = ff
  where Foo { {- found -} field = ff } = f
foo f = field
  where Foo{{- found -} ..} = f
foo f = field {- found -} f

bar :: Int -> Foo
bar i = Foo i
bar i = Foo { {- found -} field = i }
bar field = Foo { {- found -} ..}

There might be a complication when using constructors where the fields are not named, as this one, where HLS reports some strange results, I guess mostly because it looks for usages of Char itself:

data Bar = Bar Char

-- textDocument/references on `Char` above

foo2 :: Bar -> {- matches -} Char
foo2 {- foo2 matches? -} (Bar f) = f
foo2 (Bar {-matches-} f) = f
-- surprisingly this one ^ is the same as the previous one but this one matches?
-- it seems the second appearance matches on the argument instead of the
-- function name?
foo2 f = ff
  where Bar {- matches -} ff = f
foo2 f = field
  where Bar {- matches -} field = f

bar2 :: {- matches -} Char -> Bar
bar2 {- bar2 matches? -} i = Bar i

However I don't expect this one to be a problem because one is forced to pattern match anyways.

michaelpj commented 1 year ago

So, my read of the case with the named field is that we (reasonably) look for references of the name. But I think it would in fact be helpful to treat a pattern match that doesn't use the field name as an implicit reference to the field name.

It would be cool to be able to find uses of a non-named field, but it seems hard to express that that's what you want instead of references to the type name.