Skip to content
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

Import suggestion for missing newtype constructor, all types constructor and indirect overloadedrecorddot fields #4516

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

guibou
Copy link
Collaborator

@guibou guibou commented Mar 11, 2025

This MR contains 3 commits which improves the import suggestion code actions:

  • suggests to import a module when the newtype constructor is not in scope. This usually happen when coerceing newtype.
  • suggests to import a module when all the data constructors for a type are not in scope. This usually happen when creating an instance of a type from another module. For example, deriving instance Generic Bar when Bar (the type) may be in scope, but not the construtors (e.g. requiring Bar(..)).
  • suggests to import a module when a field of a type is not in scope and used in an OverloadedRecordDot context, such as foo.bar where foo :: Foo and Foo(bar) is not in scope. Note that this is already partially handled by HLS in the case where the module containing Foo is already imported, but not the fields. In this context, GHC is kind enough to give us an hint (something like "maybe add field bar to import line...") and this is already handled by HLS. However, if the module containing Foo is not already imported, it does not work.

All of these case usually happen when types are "indirectly" in scope. For example, consider the followings:

module T where

data Foo = Foo { foo :: Int }
module Value where
import T

foo = Foo 10
module Usage where

import Value

result = foo.foo

In the module Usage, the type Foo is not explicitly imported, it exists just indirectly because foo is imported from Value.

Discussion

I tried to take care of multiples patterns in the regex used to match the error messages, such as qualified or not qualified types or class, as well as polymorphic or not. If you see a case not taken into account, please tell me.

Tests

I've added simple tests for the coerce and instance Generic "cases" and a more involved test for the overloaded record dot cases.

I've developed this using GHC 9.10. I'll test on other version of GHC asap, perhaps some adaptations of the regex need to be introduced.

@guibou guibou requested a review from santiweight as a code owner March 11, 2025 15:23
@fendor fendor added the status: needs review This PR is ready for review label Mar 11, 2025
guibou added 4 commits March 12, 2025 12:12
In a context where we want to `coerce` to a type for which constructor
is not in scope, GHC fails with (e.g., for `Sum Int`):

```
• Couldn't match representation of type ‘Int’ with that of ‘Sum Int’
    arising from a use of ‘coerce’
    The data constructor ‘base-4.18.2.1:Data.Semigroup.Internal.Sum’
      of newtype ‘Sum’ is not in scope
```

This code action detects the missing `newtype` and suggests to add the
required import.

This is convenient because otherwise the user need to interpret the
error message and most of the time manually find which module and type to import.

Note that a better implementation could try to decet that the type is
already imported (if that's the case) and just suggest to add the
constructor (e.g. `(..)`) in the import list, but this is too much
complexity to implement. It could lead to duplicated import lines which
will be "cleaned" by formatter or other extensions.
For example, `deriving instance Generic (Sum Int)`, but it should work
for other deriving which indirectly requires a complete access to the
type constructor.

```
Can't make a derived instance of ‘Generic (Sum Int)’:
    The data constructors of ‘Sum’ are not all in scope
      so you cannot derive an instance for it
```
For example, the following code `foo.titi` when the type of `foo` (e.g.
`Bar` here is not in scope and not from an already imported module (e.g.
the type exists indirectly because here `foo :: Bar` comes from another module).

If the module which contains `Bar` is already imported, GHC already
gives an hint to add `titi` to the `import Bar` line and this is already
correctly handled by HLS.

```
 No instance for ‘HasField "titi" Bar.Bar String’
    arising from selecting the field ‘titi’
```
This correct previous commit by handling ghc 9.4 parethensis instead of
"tick".
@guibou guibou force-pushed the import_suggestion_for_coerce branch from bddf4b5 to f1eb36a Compare March 12, 2025 08:13
@guibou
Copy link
Collaborator Author

guibou commented Mar 12, 2025

Rebased and fixed test for GHC 9.4.

Copy link
Collaborator

@soulomoon soulomoon left a comment

Choose a reason for hiding this comment

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

Thanks for the change ~
I think we just need more test on the regex.
Otherwise it is good to go.

-- "foo" (Bar Int)...`, e.g. see the parenthesis
| Just [_module, name] <- matchRegexUnifySpaces x "No instance for [‘(].*HasField \"[^\"]+\" ([^ (.]+\\.)*([^ (.]+).*[’)]"
= Just $ NotInScopeThing name
| Just [_module, name] <- matchRegexUnifySpaces x "No instance for [‘(].*HasField \"[^\"]+\" \\(([^ .]+\\.)*([^ .]+)[^)]*\\).*[’)]"
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems complex.
perhapes more unit test on these patterns. Thanks
I mean just plain unit test for matchRegexUnifySpaces x "No instance for [‘(].*HasField \"[^\"]+\" ([^ (.]+\\.)*([^ (.]+).*[’)]" and matchRegexUnifySpaces x "No instance for [‘(].*HasField \"[^\"]+\" \\(([^ .]+\\.)*([^ .]+)[^)]*\\).*[’)]"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: needs review This PR is ready for review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants