Skip to content

Add code action to remove redundant qualified in imports - #5145

Open
BHARATH0153 wants to merge 6 commits into
haskell:masterfrom
BHARATH0153:codeaction-duplicate-qualified
Open

BHARATH0153 wants to merge 6 commits into
haskell:masterfrom
BHARATH0153:codeaction-duplicate-qualified

Conversation

@BHARATH0153

@BHARATH0153 BHARATH0153 commented Oct 13, 2026 •

Copy link
Copy Markdown

Closes #5121.

Malformed imports such as

import qualified Data.List qualified

produce the parser error Multiple occurrences of 'qualified'. This adds a
Remove redundant 'qualified' code action that drops one of the two
occurrences:

  • the prepositive qualified when ImportQualifiedPost is enabled, producing
    import Data.List qualified;
  • the postpositive qualified otherwise, producing import qualified Data.List.

Because the module fails to parse, no DynFlags are available for the file, so
the action decides which occurrence to remove by inspecting the diagnostics in
the requested range: when ImportQualifiedPost is off GHC additionally reports
the postpositive qualified as not enabled. This exposes the in-range
diagnostics to code actions through a new caaDiagnostics field and
[FileDiagnostic] -> r instance.

@BHARATH0153
BHARATH0153 force-pushed the codeaction-duplicate-qualified branch from a608d8b to 1b3c6ab Compare October 13, 2026 14:40
@fendor

fendor commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Hi, thank you for your contribution!

I think it would be generally good if you discussed your implementation approach on the ticket before actually working on it.

Because the module fails to parse, no DynFlags are available for the file

That's not true at all, in most cases we have old DynFlags around from the last time it parsed!
Rather rarely do we have no DynFlags available at all.

I have also a question regarding the implementation, you seem to manually splice out the qualified, have you considered other approaches?

@BHARATH0153

BHARATH0153 commented Oct 24, 2026 •

Copy link
Copy Markdown
Author

@fendor Thanks for the review!

You're right about DynFlags. I updated the implementation to pull them from caaDf and use qualifiedImportStyle to decide whether to remove the prefix or postfix qualified, with a fallback if they're unavailable. That avoids having to infer it from diagnostics.

As for the manual splicing, since the module fails to parse with "Multiple occurrences of 'qualified'", there's usually no reliable parsed AST to run exactprint rewrites against. A small, whitespace-aware text edit on the specific line seemed like the most robust and minimal approach for this particular parser error.

I did consider an AST-based approach, but in the unparseable case it wasn't straightforward. If there's an existing pattern for fixing syntax errors like this in HLS, I'd be happy to refactor to follow it!

Also noted about discussing the approach on the ticket first, I'll do that going forward. Thanks!

@BHARATH0153
BHARATH0153 force-pushed the codeaction-duplicate-qualified branch from b684dc9 to a3d1ba7 Compare October 24, 2026 16:14

@fendor fendor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think the approach should be reconsidered, there is lots of parsing and ad-hoc logic happening.

Trying to parse any kind of haskell syntax ad-hoc is a bad idea, in my mind.
If we can't use the AST directly, we should be doing as little as possible.

Given that the source range points exactly at the qualified, shouldn't this just be a matter of generating a text edit that replaces the diagnostic range with ""?

Comment on lines +1020 to +1021
importQualifiedPostFromPragmas :: T.Text -> Bool
importQualifiedPostFromPragmas contents =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we should implement this fall back behaviour, I think it is fine to not offer anything if we can't find the DynFlags. This ad-hoc parsing usually just leads to more tears down the line.

Comment on lines +1004 to +1019
redundantQualifiedEdit contents = do
let Position startLine _ = _start _range
line <- listToMaybe $ drop (fromIntegral startLine) $ T.lines contents
(preQual, postQual) <- case qualifiedOccurrences line of
a : b : _ -> Just (a, b)
_ -> Nothing
let (colStart, colEnd)
| usePost = preQual
| otherwise = postQual
baseRange = Range (Position startLine (fromIntegral colStart)) (Position startLine (fromIntegral colEnd))
trailing = T.takeWhile isSpace (T.drop colEnd line)
leading = T.takeWhileEnd isSpace (T.take colStart line)
editRange
| usePost = baseRange { _end = Position startLine (fromIntegral (colEnd + T.length trailing)) }
| otherwise = baseRange { _start = Position startLine (fromIntegral (colStart - T.length leading)) }
pure $ TextEdit editRange ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This might need a comment what this is trying to do. Seems very complicated.

Comment on lines +993 to +995
| "Multiple occurrences of 'qualified'" `T.isInfixOf` _message
, Just contents <- contents'
, Just edit <- redundantQualifiedEdit contents

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we use the structured diagnostic in FileDiagnostic instead of parsing the message?
Also, the diagnostic has a source range, isn't that sufficient to calculate the edit?

Comment on lines +1001 to +1003
-- The module could not be parsed and no DynFlags were available, so
-- fall back to the LANGUAGE/OPTIONS_GHC pragmas in the file itself.
Nothing -> maybe False importQualifiedPostFromPragmas contents'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am in all honesty pretty much against this ad-hoc parsing of line pragmas. If there are no DynFlags, just assume a default.

This branch has not been deployed

No deployments
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.

Add codeaction for mutliple occurences of qualifed in import

2 participants